From 9f04a7ec3429303118f12607703b877d8ba43888 Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Thu, 18 Nov 2010 12:09:08 +0000
Subject: Basic User Search support, and Find Rooms cleanup.

Adds a throbber to the MUC search, turns the Add Contact dialog into something searchy, adds the option to open chats to arbitrary JIDs.

Resolves: #614
Resolves: #695
Resolves: #436

Release-Notes: On servers that support it, users can now perform searches for contacts to add or chat to.

diff --git a/Swift/Controllers/Chat/MUCSearchController.cpp b/Swift/Controllers/Chat/MUCSearchController.cpp
index 7368dbb..c254e51 100644
--- a/Swift/Controllers/Chat/MUCSearchController.cpp
+++ b/Swift/Controllers/Chat/MUCSearchController.cpp
@@ -11,22 +11,26 @@
 #include <boost/bind.hpp>
 #include <boost/shared_ptr.hpp>
 
-#include "Swiften/Disco/GetDiscoInfoRequest.h"
-#include "Swiften/Disco/GetDiscoItemsRequest.h"
+#include <Swiften/Disco/GetDiscoInfoRequest.h>
+#include <Swiften/Disco/GetDiscoItemsRequest.h>
 
-#include "Swift/Controllers/UIEvents/UIEventStream.h"
-#include "Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h"
-#include "Swift/Controllers/UIInterfaces/MUCSearchWindow.h"
-#include "Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h"
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h>
+#include <Swift/Controllers/UIInterfaces/MUCSearchWindow.h>
+#include <Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h>
+#include <Swift/Controllers/DiscoServiceWalker.h>
+#include <Swiften/Client/NickResolver.h>
 
 namespace Swift {
 
 static const String SEARCHED_SERVICES = "searchedServices";
 
-MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* factory, IQRouter* iqRouter, SettingsProvider* settings) : jid_(jid) {
+MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* factory, IQRouter* iqRouter, SettingsProvider* settings, NickResolver *nickResolver) : jid_(jid) {
 	iqRouter_ = iqRouter;
 	settings_ = settings;
 	uiEventStream_ = uiEventStream;
+	nickResolver_ = nickResolver;
+	itemsInProgress_ = 0;
 	uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&MUCSearchController::handleUIEvent, this, _1));
 	window_ = NULL;
 	factory_ = factory;
@@ -34,6 +38,9 @@ MUCSearchController::MUCSearchController(const JID& jid, UIEventStream* uiEventS
 }
 
 MUCSearchController::~MUCSearchController() {
+	foreach (DiscoServiceWalker* walker, walksInProgress_) {
+		delete walker;
+	}
 	delete window_;
 }
 
@@ -42,12 +49,12 @@ void MUCSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
 	if (searchEvent) {
 		if (!window_) {
 			window_ = factory_->createMUCSearchWindow(uiEventStream_);
-			window_->onAddService.connect(boost::bind(&MUCSearchController::handleAddService, this, _1, true));
+			window_->onAddService.connect(boost::bind(&MUCSearchController::handleAddService, this, _1));
 			window_->addSavedServices(savedServices_);
-			handleAddService(JID(jid_.getDomain()), true);
+			handleAddService(JID(jid_.getDomain()));
 		}
 		window_->setMUC("");
-		window_->setNick(jid_.getNode());
+		window_->setNick(nickResolver_->jidToNick(jid_));
 		window_->show();
 		return;
 	}
@@ -79,76 +86,66 @@ void MUCSearchController::addAndSaveServices(const JID& jid) {
 	window_->addSavedServices(savedServices_);
 }
 
-void MUCSearchController::handleAddService(const JID& jid, bool userTriggered) {
-	if (userTriggered) {
-		addAndSaveServices(jid);
-	}
-	if (std::find(services_.begin(), services_.end(), jid) != services_.end()) {
-		if (!userTriggered) {
-			/* No infinite recursion. (Some buggy servers do infinitely deep disco of themselves)*/
-			return;
-		}
-	} else if (userTriggered) {
-		services_.push_back(jid);
-		serviceDetails_[jid].setComplete(false);
-		refreshView();
-	}
+void MUCSearchController::handleAddService(const JID& jid) {
 	if (!jid.isValid()) {
 		//Set Window to say error this isn't valid
 		return;
 	}
-	GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(jid, iqRouter_);
-	discoInfoRequest->onResponse.connect(boost::bind(&MUCSearchController::handleDiscoInfoResponse, this, _1, _2, jid));
-	discoInfoRequest->send();
-}
-
-void MUCSearchController::removeService(const JID& jid) {
-	serviceDetails_.erase(jid);
-	services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); 
+	addAndSaveServices(jid);
+	services_.push_back(jid);
+	serviceDetails_[jid].setComplete(false);
+	window_->setSearchInProgress(true);
 	refreshView();
+	DiscoServiceWalker* walker = new DiscoServiceWalker(jid, iqRouter_);
+	walker->onServiceFound.connect(boost::bind(&MUCSearchController::handleDiscoServiceFound, this, _1, _2));
+	walker->onWalkComplete.connect(boost::bind(&MUCSearchController::handleDiscoWalkFinished, this, walker));
+	walksInProgress_.push_back(walker);
+	walker->beginWalk();
 }
 
-void MUCSearchController::handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid) {
-	if (error) {
-		handleDiscoError(jid, error);
-		return;
-	}
-	GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_);
-	bool mucService = false;
-	bool couldContainServices = false;
+void MUCSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info) {
+	bool isMUC;
 	String name;
 	foreach (DiscoInfo::Identity identity, info->getIdentities()) {
-		if ((identity.getCategory() == "directory" 
-			&& identity.getType() == "chatroom")
-			|| (identity.getCategory() == "conference" 
-			&& identity.getType() == "text")) {
-			mucService = true;
-			name = identity.getName();
-		}
-		if (identity.getCategory() == "server") {
-			couldContainServices = true;
-			name = identity.getName();
-		}
+			if ((identity.getCategory() == "directory"
+				&& identity.getType() == "chatroom")
+				|| (identity.getCategory() == "conference"
+				&& identity.getType() == "text")) {
+				isMUC = true;
+				name = identity.getName();
+			}
 	}
-	services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); /* Bring it back to the end on a refresh */
-	services_.push_back(jid);
-	serviceDetails_[jid].setName(name);
-	serviceDetails_[jid].setJID(jid);
-	serviceDetails_[jid].setComplete(false);
-
-	if (mucService) {
+	if (isMUC) {
+		services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); /* Bring it back to the end on a refresh */
+		services_.push_back(jid);
+		serviceDetails_[jid].setName(name);
+		serviceDetails_[jid].setJID(jid);
+		serviceDetails_[jid].setComplete(false);
+		itemsInProgress_++;
+		GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_);
 		discoItemsRequest->onResponse.connect(boost::bind(&MUCSearchController::handleRoomsItemsResponse, this, _1, _2, jid));
-	} else if (couldContainServices) {
-		discoItemsRequest->onResponse.connect(boost::bind(&MUCSearchController::handleServerItemsResponse, this, _1, _2, jid));
+		discoItemsRequest->send();
 	} else {
 		removeService(jid);
-		return;
 	}
-	discoItemsRequest->send();
+	refreshView();
+}
+
+void MUCSearchController::handleDiscoWalkFinished(DiscoServiceWalker* walker) {
+	walksInProgress_.erase(std::remove(walksInProgress_.begin(), walksInProgress_.end(), walker), walksInProgress_.end());
+	updateInProgressness();
+	delete walker;
+}
+
+void MUCSearchController::removeService(const JID& jid) {
+	serviceDetails_.erase(jid);
+	services_.erase(std::remove(services_.begin(), services_.end(), jid), services_.end()); 
 	refreshView();
 }
 
 void MUCSearchController::handleRoomsItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) {
+	itemsInProgress_--;
+	updateInProgressness();
 	if (error) {
 		handleDiscoError(jid, error);
 		return;
@@ -161,25 +158,6 @@ void MUCSearchController::handleRoomsItemsResponse(boost::shared_ptr<DiscoItems>
 	refreshView();
 }
 
-void MUCSearchController::handleServerItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) {
-	if (error) {
-		handleDiscoError(jid, error);
-		return;
-	}
-	if (jid.isValid()) {
-		removeService(jid);
-	}
-	foreach (DiscoItems::Item item, items->getItems()) {
-		if (item.getNode().isEmpty()) {
-			/* Don't look at noded items. It's possible that this will exclude some services,
-			 * but I've never seen one in the wild, and it's an easy fix for not looping.
-			 */
-			handleAddService(item.getJID());
-		}
-	}
-	refreshView();
-}
-
 void MUCSearchController::handleDiscoError(const JID& jid, ErrorPayload::ref error) {
 	serviceDetails_[jid].setComplete(true);
 	serviceDetails_[jid].setError(error->getText());
@@ -192,4 +170,8 @@ void MUCSearchController::refreshView() {
 	}
 }
 
+void MUCSearchController::updateInProgressness() {
+	window_->setSearchInProgress(walksInProgress_.size() + itemsInProgress_ > 0);
+}
+
 }
diff --git a/Swift/Controllers/Chat/MUCSearchController.h b/Swift/Controllers/Chat/MUCSearchController.h
index f09a801..6caee54 100644
--- a/Swift/Controllers/Chat/MUCSearchController.h
+++ b/Swift/Controllers/Chat/MUCSearchController.h
@@ -27,6 +27,8 @@ namespace Swift {
 	class MUCSearchWindow;
 	class MUCSearchWindowFactory;
 	class IQRouter;
+	class DiscoServiceWalker;
+	class NickResolver;
 
 	class MUCService {
 		public:
@@ -86,28 +88,32 @@ namespace Swift {
 
 	class MUCSearchController {
 		public:
-			MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* mucSearchWindowFactory, IQRouter* iqRouter, SettingsProvider* settings);
+			MUCSearchController(const JID& jid, UIEventStream* uiEventStream, MUCSearchWindowFactory* mucSearchWindowFactory, IQRouter* iqRouter, SettingsProvider* settings, NickResolver* nickResolver);
 			~MUCSearchController();
 		private:
 			void handleUIEvent(boost::shared_ptr<UIEvent> event);
-			void handleAddService(const JID& jid, bool userTriggered=false);
-			void handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid);
+			void handleAddService(const JID& jid);
 			void handleRoomsItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid);
-			void handleServerItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid);
 			void handleDiscoError(const JID& jid, ErrorPayload::ref error);
+			void handleDiscoServiceFound(const JID&, boost::shared_ptr<DiscoInfo>);
+			void handleDiscoWalkFinished(DiscoServiceWalker* walker);
 			void removeService(const JID& jid);
 			void refreshView();
 			void loadServices();
 			void addAndSaveServices(const JID& jid);
+			void updateInProgressness();
 			UIEventStream* uiEventStream_;
 			MUCSearchWindow* window_;
 			MUCSearchWindowFactory* factory_;
 			SettingsProvider* settings_;
+			NickResolver* nickResolver_;
 			boost::bsignals::scoped_connection uiEventConnection_;
 			std::vector<JID> services_;
 			std::vector<JID> savedServices_;
 			std::map<JID, MUCService> serviceDetails_;
+			std::vector<DiscoServiceWalker*> walksInProgress_;
 			IQRouter* iqRouter_;
 			JID jid_;
+			int itemsInProgress_;
 	};
 }
diff --git a/Swift/Controllers/Chat/UserSearchController.cpp b/Swift/Controllers/Chat/UserSearchController.cpp
new file mode 100644
index 0000000..c3e40c8
--- /dev/null
+++ b/Swift/Controllers/Chat/UserSearchController.cpp
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swift/Controllers/Chat/UserSearchController.h"
+
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include <Swiften/Disco/GetDiscoInfoRequest.h>
+#include <Swiften/Disco/GetDiscoItemsRequest.h>
+
+#include <Swift/Controllers/DiscoServiceWalker.h>
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h>
+#include <Swift/Controllers/UIInterfaces/UserSearchWindow.h>
+#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h>
+
+namespace Swift {
+UserSearchController::UserSearchController(const JID& jid, UIEventStream* uiEventStream, UserSearchWindowFactory* factory, IQRouter* iqRouter) : jid_(jid) {
+	iqRouter_ = iqRouter;
+	uiEventStream_ = uiEventStream;
+	uiEventConnection_ = uiEventStream_->onUIEvent.connect(boost::bind(&UserSearchController::handleUIEvent, this, _1));
+	window_ = NULL;
+	factory_ = factory;
+	discoWalker_ = NULL;
+}
+
+UserSearchController::~UserSearchController() {
+	delete window_;
+	delete discoWalker_;
+}
+
+void UserSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
+	boost::shared_ptr<RequestUserSearchUIEvent> searchEvent = boost::dynamic_pointer_cast<RequestUserSearchUIEvent>(event);
+	if (searchEvent) {
+		if (!window_) {
+			window_ = factory_->createUserSearchWindow(uiEventStream_);
+			window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1));
+			window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2));
+			window_->setSelectedService(JID(jid_.getDomain()));
+			window_->clear();
+		}
+		window_->show();
+		return;
+	}
+}
+
+void UserSearchController::handleFormRequested(const JID& service) {
+	window_->setSearchError(false);
+	window_->setServerSupportsSearch(true);
+	//Abort a previous search if is active
+	delete discoWalker_;
+	discoWalker_ = new DiscoServiceWalker(service, iqRouter_);
+	discoWalker_->onServiceFound.connect(boost::bind(&UserSearchController::handleDiscoServiceFound, this, _1, _2, discoWalker_));
+	discoWalker_->onWalkComplete.connect(boost::bind(&UserSearchController::handleDiscoWalkFinished, this, discoWalker_));
+	discoWalker_->beginWalk();
+}
+
+void UserSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info, DiscoServiceWalker* walker) {
+	bool isUserDirectory = false;
+	bool supports55 = false;
+	foreach (DiscoInfo::Identity identity, info->getIdentities()) {
+		if ((identity.getCategory() == "directory"
+			&& identity.getType() == "user")) {
+			isUserDirectory = true;
+		}
+	}
+	std::vector<String> features = info->getFeatures();
+	supports55 = std::find(features.begin(), features.end(), DiscoInfo::JabberSearchFeature) != features.end();
+	if (/*isUserDirectory && */supports55) { //FIXME: once M-Link correctly advertises directoryness.
+		/* Abort further searches.*/
+		delete discoWalker_;
+		discoWalker_ = NULL;
+		boost::shared_ptr<GenericRequest<SearchPayload> > searchRequest(new GenericRequest<SearchPayload>(IQ::Get, jid, boost::shared_ptr<SearchPayload>(new SearchPayload()), iqRouter_));
+		searchRequest->onResponse.connect(boost::bind(&UserSearchController::handleFormResponse, this, _1, _2, jid));
+		searchRequest->send();
+	}
+}
+
+void UserSearchController::handleFormResponse(boost::shared_ptr<SearchPayload> fields, ErrorPayload::ref error, const JID& jid) {
+	if (error || !fields) {
+		window_->setServerSupportsSearch(false);
+		return;
+	}
+	window_->setSearchFields(fields);
+}
+
+void UserSearchController::handleSearch(boost::shared_ptr<SearchPayload> fields, const JID& jid) {
+	boost::shared_ptr<GenericRequest<SearchPayload> > searchRequest(new GenericRequest<SearchPayload>(IQ::Set, jid, fields, iqRouter_));
+	searchRequest->onResponse.connect(boost::bind(&UserSearchController::handleSearchResponse, this, _1, _2, jid));
+	searchRequest->send();
+}
+
+void UserSearchController::handleSearchResponse(boost::shared_ptr<SearchPayload> resultsPayload, ErrorPayload::ref error, const JID& jid) {
+	if (error || !resultsPayload) {
+		window_->setSearchError(true);
+		return;
+	}
+	std::vector<UserSearchResult> results;
+	foreach (SearchPayload::Item item, resultsPayload->getItems()) {
+		JID jid(item.jid);
+		std::map<String, String> fields;
+		fields["first"] = item.first;
+		fields["last"] = item.last;
+		fields["nick"] = item.nick;
+		fields["email"] = item.email;
+		UserSearchResult result(jid, fields);
+		results.push_back(result);
+	}
+	window_->setResults(results);
+}
+
+void UserSearchController::handleDiscoWalkFinished(DiscoServiceWalker* walker) {
+	window_->setServerSupportsSearch(false);
+	delete discoWalker_;
+	discoWalker_ = NULL;
+}
+
+}
diff --git a/Swift/Controllers/Chat/UserSearchController.h b/Swift/Controllers/Chat/UserSearchController.h
new file mode 100644
index 0000000..3ba3352
--- /dev/null
+++ b/Swift/Controllers/Chat/UserSearchController.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+#include <map>
+#include <vector>
+#include <Swiften/Base/boost_bsignals.h>
+
+#include <Swiften/Elements/SearchPayload.h>
+#include <Swiften/Base/String.h>
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/DiscoInfo.h>
+#include <Swiften/Elements/DiscoItems.h>
+#include <Swiften/Elements/ErrorPayload.h>
+
+namespace Swift {
+	class UIEventStream;
+	class UIEvent;
+	class UserSearchWindow;
+	class UserSearchWindowFactory;
+	class IQRouter;
+	class DiscoServiceWalker;
+
+	class UserSearchResult {
+		public:
+			UserSearchResult(const JID& jid, const std::map<String, String>& fields) : jid_(jid), fields_(fields) {}
+			const JID& getJID() const {return jid_;}
+			const std::map<String, String>& getFields() const {return fields_;}
+		private:
+			JID jid_;
+			std::map<String, String> fields_;
+	};
+
+	class UserSearchController {
+		public:
+			UserSearchController(const JID& jid, UIEventStream* uiEventStream, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter);
+			~UserSearchController();
+		private:
+			void handleUIEvent(boost::shared_ptr<UIEvent> event);
+			void handleFormRequested(const JID& service);
+			void handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info, DiscoServiceWalker* walker);
+			void handleDiscoWalkFinished(DiscoServiceWalker* walker);
+			void handleFormResponse(boost::shared_ptr<SearchPayload> items, ErrorPayload::ref error, const JID& jid);
+			void handleSearch(boost::shared_ptr<SearchPayload> fields, const JID& jid);
+			void handleSearchResponse(boost::shared_ptr<SearchPayload> results, ErrorPayload::ref error, const JID& jid);
+			UIEventStream* uiEventStream_;
+			UserSearchWindow* window_;
+			UserSearchWindowFactory* factory_;
+			boost::bsignals::scoped_connection uiEventConnection_;
+			IQRouter* iqRouter_;
+			JID jid_;
+			DiscoServiceWalker* discoWalker_;
+	};
+}
diff --git a/Swift/Controllers/DiscoServiceWalker.cpp b/Swift/Controllers/DiscoServiceWalker.cpp
new file mode 100644
index 0000000..95ce23b
--- /dev/null
+++ b/Swift/Controllers/DiscoServiceWalker.cpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swift/Controllers/DiscoServiceWalker.h>
+
+#include <boost/bind.hpp>
+
+#include "Swiften/Disco/GetDiscoInfoRequest.h"
+#include "Swiften/Disco/GetDiscoItemsRequest.h"
+
+namespace Swift {
+
+DiscoServiceWalker::DiscoServiceWalker(const JID& service, IQRouter* iqRouter, size_t maxSteps) : service_(service), iqRouter_(iqRouter), maxSteps_(maxSteps) {
+
+}
+
+void DiscoServiceWalker::beginWalk() {
+	assert(servicesBeingSearched_.size() == 0);
+	walkNode(service_);
+}
+
+void DiscoServiceWalker::walkNode(const JID& jid) {
+	servicesBeingSearched_.push_back(jid);
+	searchedServices_.push_back(jid);
+	GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(jid, iqRouter_);
+	discoInfoRequest->onResponse.connect(boost::bind(&DiscoServiceWalker::handleDiscoInfoResponse, this, _1, _2, jid));
+	discoInfoRequest->send();
+}
+
+void DiscoServiceWalker::handleReceivedDiscoItem(const JID& item) {
+	if (std::find(searchedServices_.begin(), searchedServices_.end(), item) != searchedServices_.end()) {
+		/* Don't recurse infinitely */
+		return;
+	}
+	walkNode(item);
+}
+
+void DiscoServiceWalker::handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid) {
+	if (error) {
+		handleDiscoError(jid, error);
+		return;
+	}
+
+	bool couldContainServices = false;
+	foreach (DiscoInfo::Identity identity, info->getIdentities()) {
+		if (identity.getCategory() == "server") {
+			couldContainServices = true;
+		}
+	}
+	bool completed = false;
+	if (couldContainServices) {
+		GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(jid, iqRouter_);
+		discoItemsRequest->onResponse.connect(boost::bind(&DiscoServiceWalker::handleDiscoItemsResponse, this, _1, _2, jid));
+		discoItemsRequest->send();
+	} else {
+		completed = true;
+	}
+	onServiceFound(jid, info);
+	if (completed) {
+		markNodeCompleted(jid);
+	}
+}
+
+void DiscoServiceWalker::handleDiscoItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid) {
+	if (error) {
+		handleDiscoError(jid, error);
+		return;
+	}
+	foreach (DiscoItems::Item item, items->getItems()) {
+		if (item.getNode().isEmpty()) {
+			/* Don't look at noded items. It's possible that this will exclude some services,
+			 * but I've never seen one in the wild, and it's an easy fix for not looping.
+			 */
+			handleReceivedDiscoItem(item.getJID());
+		}
+	}
+	markNodeCompleted(jid);
+}
+
+void DiscoServiceWalker::handleDiscoError(const JID& jid, ErrorPayload::ref /*error*/) {
+	markNodeCompleted(jid);
+}
+
+void DiscoServiceWalker::markNodeCompleted(const JID& jid) {
+	servicesBeingSearched_.erase(std::remove(servicesBeingSearched_.begin(), servicesBeingSearched_.end(), jid), servicesBeingSearched_.end());
+	/* All results are in */
+	if (servicesBeingSearched_.size() == 0) {
+		onWalkComplete();
+	}
+	/* Check if we're on a rampage */
+	if (searchedServices_.size() >= maxSteps_) {
+		onWalkComplete();
+	}
+}
+
+}
diff --git a/Swift/Controllers/DiscoServiceWalker.h b/Swift/Controllers/DiscoServiceWalker.h
new file mode 100644
index 0000000..5b2a47e
--- /dev/null
+++ b/Swift/Controllers/DiscoServiceWalker.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include <boost/shared_ptr.hpp>
+#include <Swiften/Base/boost_bsignals.h>
+#include <Swiften/Base/String.h>
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/DiscoInfo.h>
+#include <Swiften/Elements/DiscoItems.h>
+#include <Swiften/Elements/ErrorPayload.h>
+
+namespace Swift {
+	class IQRouter;
+	/**
+	 * Recursively walk service discovery trees to find all services offered.
+	 * This stops on any disco item that's not reporting itself as a server.
+	 */
+	class DiscoServiceWalker : public boost::signals::trackable {
+		public:
+			DiscoServiceWalker(const JID& service, IQRouter* iqRouter, size_t maxSteps = 200);
+			/** Start the walk. Call this exactly once.*/
+			void beginWalk();
+			/** Emitted for each service found. */
+			boost::signal<void(const JID&, boost::shared_ptr<DiscoInfo>)> onServiceFound;
+			/** Emitted when walking is complete.*/
+			boost::signal<void()> onWalkComplete;
+		private:
+			void handleReceivedDiscoItem(const JID& item);
+			void walkNode(const JID& jid);
+			void markNodeCompleted(const JID& jid);
+			void handleDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error, const JID& jid);
+			void handleDiscoItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error, const JID& jid);
+			void handleDiscoError(const JID& jid, ErrorPayload::ref error);
+			JID service_;
+			IQRouter* iqRouter_;
+			size_t maxSteps_;
+			std::vector<JID> servicesBeingSearched_;
+			std::vector<JID> searchedServices_;
+	};
+}
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 6e57a8f..b925857 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -18,6 +18,7 @@
 #include "Swiften/Client/Storages.h"
 #include "Swiften/VCards/VCardManager.h"
 #include "Swift/Controllers/Chat/MUCSearchController.h"
+#include "Swift/Controllers/Chat/UserSearchController.h"
 #include "Swift/Controllers/Chat/ChatsManager.h"
 #include "Swift/Controllers/XMPPEvents/EventController.h"
 #include "Swift/Controllers/EventWindowController.h"
@@ -98,6 +99,7 @@ MainController::MainController(
 	chatsManager_ = NULL;
 	eventWindowController_ = NULL;
 	mucSearchController_ = NULL;
+	userSearchController_ = NULL;
 	quitRequested_ = false;
 
 	timeBeforeNextReconnect_ = -1;
@@ -188,6 +190,8 @@ void MainController::resetClient() {
 	statusTracker_ = NULL;
 	delete profileSettings_;
 	profileSettings_ = NULL;
+	delete userSearchController_;
+	userSearchController_ = NULL;
 }
 
 void MainController::handleUIEvent(boost::shared_ptr<UIEvent> event) {
@@ -242,7 +246,9 @@ void MainController::handleConnected() {
 		client_->getDiscoManager()->setCapsNode(CLIENT_NODE);
 		client_->getDiscoManager()->setDiscoInfo(discoInfo);
 
-		mucSearchController_ = new MUCSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), settings_);
+
+		mucSearchController_ = new MUCSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), settings_, client_->getNickResolver());
+		userSearchController_ = new UserSearchController(jid_, uiEventStream_, uiFactory_, client_->getIQRouter());
 	}
 	
 	client_->requestRoster();
diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h
index 2f101a5..aece80f 100644
--- a/Swift/Controllers/MainController.h
+++ b/Swift/Controllers/MainController.h
@@ -54,6 +54,7 @@ namespace Swift {
 	class EventWindowFactory;
 	class EventWindowController;
 	class MUCSearchController;
+	class UserSearchController;
 	class StatusTracker;
 	class Dock;
 	class Storages;
@@ -137,6 +138,7 @@ namespace Swift {
 			boost::shared_ptr<ErrorEvent> lastDisconnectError_;
 			bool useDelayForLatency_;
 			MUCSearchController* mucSearchController_;
+			UserSearchController* userSearchController_;
 			int timeBeforeNextReconnect_;
 			Timer::ref reconnectTimer_;
 			StatusTracker* statusTracker_;
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index 9cd2be4..96674bd 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -26,6 +26,8 @@ if env["SCONS_STAGE"] == "build" :
 			"Chat/ChatsManager.cpp",
 			"Chat/MUCController.cpp",
 			"Chat/MUCSearchController.cpp",
+			"Chat/UserSearchController.cpp",
+			"DiscoServiceWalker.cpp",
 			"MainController.cpp",
 			"RosterController.cpp",
 			"RosterGroupExpandinessPersister.cpp",
diff --git a/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h b/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h
new file mode 100644
index 0000000..1312a84
--- /dev/null
+++ b/Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swift/Controllers/UIEvents/UIEvent.h"
+
+namespace Swift {
+	class RequestUserSearchUIEvent : public UIEvent {
+
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/MUCSearchWindow.h b/Swift/Controllers/UIInterfaces/MUCSearchWindow.h
index da54ded..3c0ab12 100644
--- a/Swift/Controllers/UIInterfaces/MUCSearchWindow.h
+++ b/Swift/Controllers/UIInterfaces/MUCSearchWindow.h
@@ -26,6 +26,7 @@ namespace Swift {
 			virtual void clearList() = 0;
 			virtual void addService(const MUCService& service) = 0;
 			virtual void addSavedServices(const std::vector<JID>& services) = 0;
+			virtual void setSearchInProgress(bool searching) = 0;
 
 			virtual void show() = 0;
 
diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h
index 4e15b27..acb7638 100644
--- a/Swift/Controllers/UIInterfaces/UIFactory.h
+++ b/Swift/Controllers/UIInterfaces/UIFactory.h
@@ -12,10 +12,11 @@
 #include <Swift/Controllers/UIInterfaces/LoginWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/MainWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h>
+#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/XMLConsoleWidgetFactory.h>
 
 namespace Swift {
-	class UIFactory : public ChatListWindowFactory, public ChatWindowFactory, public EventWindowFactory, public LoginWindowFactory, public MainWindowFactory, public MUCSearchWindowFactory, public XMLConsoleWidgetFactory {
+	class UIFactory : public ChatListWindowFactory, public ChatWindowFactory, public EventWindowFactory, public LoginWindowFactory, public MainWindowFactory, public MUCSearchWindowFactory, public XMLConsoleWidgetFactory, public UserSearchWindowFactory {
 		public:
 			virtual ~UIFactory() {}
 	};
diff --git a/Swift/Controllers/UIInterfaces/UserSearchWindow.h b/Swift/Controllers/UIInterfaces/UserSearchWindow.h
new file mode 100644
index 0000000..dda3409
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/UserSearchWindow.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swiften/Base/boost_bsignals.h"
+
+#include <vector>
+
+#include "Swiften/Base/String.h"
+#include "Swiften/JID/JID.h"
+#include "Swift/Controllers/Chat/UserSearchController.h"
+
+namespace Swift {
+
+	class UserSearchWindow {
+		public:
+			virtual ~UserSearchWindow() {};
+
+			virtual void clear() = 0;
+			virtual void setResults(const std::vector<UserSearchResult>& results) = 0;
+			virtual void addSavedServices(const std::vector<JID>& services) = 0;
+			virtual void setSelectedService(const JID& service) = 0;
+			virtual void setServerSupportsSearch(bool support) = 0;
+			virtual void setSearchError(bool support) = 0;
+			virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields) = 0;
+			virtual void show() = 0;
+
+			boost::signal<void (const JID&)> onFormRequested;
+			boost::signal<void (boost::shared_ptr<SearchPayload>, const JID&)> onSearchRequested;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h b/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h
new file mode 100644
index 0000000..808c1db
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swift/Controllers/UIInterfaces/UserSearchWindow.h"
+
+namespace Swift {
+	class UIEventStream;
+	class UserSearchWindowFactory {
+		public:
+			virtual ~UserSearchWindowFactory() {};
+
+			virtual UserSearchWindow* createUserSearchWindow(UIEventStream* eventStream) = 0;
+	};
+}
diff --git a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp
index c31230c..7d2caba 100644
--- a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp
+++ b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.cpp
@@ -7,6 +7,9 @@
 #include "Swift/QtUI/MUCSearch/QtMUCSearchWindow.h"
 
 #include <qdebug.h>
+#include <QMovie>
+#include <QScrollBar>
+#include <QTimer>
 
 #include "Swift/Controllers/UIEvents/UIEventStream.h"
 #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h"
@@ -41,12 +44,41 @@ QtMUCSearchWindow::QtMUCSearchWindow(UIEventStream* eventStream) {
 	connect(joinButton_, SIGNAL(clicked()), this, SLOT(handleJoin()));
 	connect(results_, SIGNAL(clicked(const QModelIndex&)), this, SLOT(handleSelected(const QModelIndex&)));
 	connect(results_, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleActivated(const QModelIndex&)));
+	throbber_ = new QLabel("Searching", results_);
+	throbber_->setMovie(new QMovie(":/icons/throbber.gif", QByteArray(), throbber_));
+	throbber_->setToolTip("Searching");
+	hasHadScrollBars_ = false;
+	updateThrobberPosition();
+	setSearchInProgress(false);
 }
 
 QtMUCSearchWindow::~QtMUCSearchWindow() {
 
 }
 
+void QtMUCSearchWindow::resizeEvent(QResizeEvent* /*event*/) {
+	updateThrobberPosition();
+}
+
+void QtMUCSearchWindow::updateThrobberPosition() {
+	bool isShown = throbber_->isVisible();
+	int resultWidth = results_->width();
+	int resultHeight = results_->height();
+	//throbberWidth = throbber_->movie()->scaledSize().width();
+	//throbberHeight = throbber_->movie()->scaledSize().height();
+	int throbberWidth = 16; /* This is nasty, but the above doesn't work! */
+	int throbberHeight = 16;
+	/* It's difficult (or I spent a while trying) to work out whether the scrollbars are currently shown and their appropriate size,
+	 * because if you listen for the expanded/collapsed signals, you seem to get them before the scrollbars are updated.
+	 * This seems an acceptable workaround.
+	 */
+	hasHadScrollBars_ |= results_->verticalScrollBar()->isVisible();
+	int hMargin = hasHadScrollBars_ ? results_->verticalScrollBar()->width() + 2 : 2;
+	int vMargin = 2; /* We don't get horizontal scrollbars */
+	throbber_->setGeometry(QRect(resultWidth - throbberWidth - hMargin, resultHeight - throbberHeight - vMargin, throbberWidth, throbberHeight)); /* include margins */
+	throbber_->setVisible(isShown);
+}
+
 void QtMUCSearchWindow::addSavedServices(const std::vector<JID>& services) {
 	service_->clear();
 	foreach (JID jid, services) {
@@ -145,6 +177,7 @@ void QtMUCSearchWindow::clearList() {
 }
 
 void QtMUCSearchWindow::addService(const MUCService& service) {
+	updateThrobberPosition();
 	MUCSearchServiceItem* serviceItem = new MUCSearchServiceItem(P2QSTRING(service.getJID().toString()));
 	foreach (MUCService::MUCRoom room, service.getRooms()) {
 		new MUCSearchRoomItem(P2QSTRING(room.getNode()), serviceItem);
@@ -153,4 +186,13 @@ void QtMUCSearchWindow::addService(const MUCService& service) {
 	results_->expandAll();
 }
 
+void QtMUCSearchWindow::setSearchInProgress(bool searching) {
+	if (searching) {
+		throbber_->movie()->start();
+	} else {
+		throbber_->movie()->stop();
+	}
+	throbber_->setVisible(searching);
+}
+
 }
diff --git a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h
index 27ccdcb..b8cf953 100644
--- a/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h
+++ b/Swift/QtUI/MUCSearch/QtMUCSearchWindow.h
@@ -25,19 +25,25 @@ namespace Swift {
 			virtual void clearList();
 			virtual void addService(const MUCService& service);
 			virtual void addSavedServices(const std::vector<JID>& services);
+			virtual void setSearchInProgress(bool searching);
 
 			virtual void show();
+		protected:
+			virtual void resizeEvent(QResizeEvent* event);
 		private slots:
 			void handleSearch(const QString& text);
 			void handleSearch();
 			void handleJoin();
 			void handleSelected(const QModelIndex& current);
 			void handleActivated(const QModelIndex& index);
+			void updateThrobberPosition();
 		private:
 			void createAutoJoin(const JID& room, boost::optional<String> passedNick);
 			MUCSearchModel* model_;
 			MUCSearchDelegate* delegate_;
 			UIEventStream* eventStream_;
+			QLabel* throbber_;
 			String lastSetNick_;
+			bool hasHadScrollBars_;
 	};
 }
diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp
index ee83f4d..6e53258 100644
--- a/Swift/QtUI/QtMainWindow.cpp
+++ b/Swift/QtUI/QtMainWindow.cpp
@@ -24,7 +24,7 @@
 #include "QtSwiftUtil.h"
 #include "QtTabWidget.h"
 #include "Roster/QtTreeWidget.h"
-#include "Swift/Controllers/UIEvents/AddContactUIEvent.h"
+#include "Swift/Controllers/UIEvents/RequestUserSearchUIEvent.h"
 #include "Swift/Controllers/UIEvents/RequestMUCSearchUIEvent.h"
 #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h"
 #include "Swift/Controllers/UIEvents/ToggleShowOfflineUIEvent.h"
@@ -84,9 +84,9 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS
 	QAction* joinMUCAction = new QAction("Join Room", this);
 	connect(joinMUCAction, SIGNAL(triggered()), SLOT(handleJoinMUCAction()));
 	actionsMenu->addAction(joinMUCAction);
-	addAction_ = new QAction("Add Contact", this);
-	connect(addAction_, SIGNAL(triggered(bool)), this, SLOT(handleAddActionTriggered(bool)));
-	actionsMenu->addAction(addAction_);
+	otherUserAction_ = new QAction("Find Other Contact", this);
+	connect(otherUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleOtherUserActionTriggered(bool)));
+	actionsMenu->addAction(otherUserAction_);
 	QAction* signOutAction = new QAction("Sign Out", this);
 	connect(signOutAction, SIGNAL(triggered()), SLOT(handleSignOutAction()));
 	actionsMenu->addAction(signOutAction);
@@ -123,22 +123,15 @@ void QtMainWindow::handleEventCountUpdated(int count) {
 	tabs_->setTabText(eventIndex, text);
 }
 
-void QtMainWindow::handleAddActionTriggered(bool checked) {
-	Q_UNUSED(checked);
-	QtAddContactDialog* addContact = new QtAddContactDialog(this);
-	connect(addContact, SIGNAL(onAddCommand(const JID&, const QString&)), SLOT(handleAddContactDialogComplete(const JID&, const QString&)));
-	addContact->show();
+void QtMainWindow::handleOtherUserActionTriggered(bool /*checked*/) {
+	boost::shared_ptr<UIEvent> event(new RequestUserSearchUIEvent());
+	uiEventStream_->send(event);
 }
 
 void QtMainWindow::handleSignOutAction() {
 	onSignOutRequest();
 }
 
-void QtMainWindow::handleAddContactDialogComplete(const JID& contact, const QString& name) {
-	boost::shared_ptr<UIEvent> event(new AddContactUIEvent(contact, Q2PSTRING(name)));
-	uiEventStream_->send(event);
-}
-
 void QtMainWindow::handleJoinMUCAction() {
 	uiEventStream_->send(boost::shared_ptr<UIEvent>(new RequestMUCSearchUIEvent()));
 }
diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h
index f494fd0..10cad66 100644
--- a/Swift/QtUI/QtMainWindow.h
+++ b/Swift/QtUI/QtMainWindow.h
@@ -54,15 +54,14 @@ namespace Swift {
 			void handleShowOfflineToggled(bool);
 			void handleJoinMUCAction();
 			void handleSignOutAction();
-			void handleAddContactDialogComplete(const JID& contact, const QString& name);
-			void handleAddActionTriggered(bool checked);
+			void handleOtherUserActionTriggered(bool checked);
 			void handleEventCountUpdated(int count);
 
 		private:
 			std::vector<QMenu*> menus_;
 			QtTreeWidget* treeWidget_;
 			QtRosterHeader* meView_;
-			QAction* addAction_;
+			QAction* otherUserAction_;
 			QAction* showOfflineAction_;
 			QtTabWidget* tabs_;
 			QWidget* contactsTabWidget_;
diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h
index 32e261c..ca6e126 100644
--- a/Swift/QtUI/QtSwift.h
+++ b/Swift/QtUI/QtSwift.h
@@ -42,6 +42,7 @@ namespace Swift {
 	class QtChatWindowFactory;
 	class QtSoundPlayer;
 	class QtMUCSearchWindowFactory;
+	class QtUserSearchWindowFactory;
 	class EventLoop;
 		
 	class QtSwift : public QObject {
diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp
index 55ed370..4f1bef5 100644
--- a/Swift/QtUI/QtUIFactory.cpp
+++ b/Swift/QtUI/QtUIFactory.cpp
@@ -19,6 +19,7 @@
 #include "QtChatWindowFactory.h"
 #include "QtSwiftUtil.h"
 #include "MUCSearch/QtMUCSearchWindow.h"
+#include "UserSearch/QtUserSearchWindow.h"
 
 namespace Swift {
 
@@ -77,4 +78,8 @@ ChatWindow* QtUIFactory::createChatWindow(const JID& contact, UIEventStream* eve
 	return chatWindowFactory->createChatWindow(contact, eventStream);
 }
 
+UserSearchWindow* QtUIFactory::createUserSearchWindow(UIEventStream* eventStream) {
+	return new QtUserSearchWindow(eventStream);
+};
+
 }
diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h
index 7f610a2..5282b31 100644
--- a/Swift/QtUI/QtUIFactory.h
+++ b/Swift/QtUI/QtUIFactory.h
@@ -33,6 +33,7 @@ namespace Swift {
 			virtual ChatListWindow* createChatListWindow(UIEventStream*);
 			virtual MUCSearchWindow* createMUCSearchWindow(UIEventStream* eventStream);
 			virtual ChatWindow* createChatWindow(const JID &contact, UIEventStream* eventStream);
+			virtual UserSearchWindow* createUserSearchWindow(UIEventStream* eventStream);
 
 		private slots:
 			void handleLoginWindowGeometryChanged();
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index b435e34..0f08556 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -111,6 +111,9 @@ sources = [
     "MUCSearch/MUCSearchModel.cpp",
     "MUCSearch/MUCSearchRoomItem.cpp",
     "MUCSearch/MUCSearchDelegate.cpp",
+    "UserSearch/QtUserSearchWindow.cpp",
+    "UserSearch/UserSearchModel.cpp",
+    "UserSearch/UserSearchDelegate.cpp",
     "ContextMenus/QtRosterContextMenu.cpp",
     "ContextMenus/QtContextMenu.cpp",
     "QtSubscriptionRequestWindow.cpp",
@@ -138,6 +141,7 @@ else :
   swiftProgram = myenv.Program("swift", sources)
 
 myenv.Uic4("MUCSearch/QtMUCSearchWindow.ui")
+myenv.Uic4("UserSearch/QtUserSearchWindow.ui")
 myenv.Uic4("QtAddContactDialog.ui")
 myenv.Uic4("QtBookmarkDetailWindow.ui")
 myenv.Qrc("DefaultTheme.qrc")
diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp
new file mode 100644
index 0000000..c3038d8
--- /dev/null
+++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.cpp
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swift/QtUI/UserSearch/QtUserSearchWindow.h"
+
+#include <qdebug.h>
+#include <QModelIndex>
+
+#include "Swift/Controllers/UIEvents/UIEventStream.h"
+#include "Swift/Controllers/UIEvents/RequestChatUIEvent.h"
+#include "Swift/Controllers/UIEvents/AddContactUIEvent.h"
+#include "Swift/QtUI/UserSearch/UserSearchModel.h"
+#include "Swift/QtUI/UserSearch/UserSearchDelegate.h"
+#include "Swift/QtUI/QtSwiftUtil.h"
+
+namespace Swift {
+
+QtUserSearchWindow::QtUserSearchWindow(UIEventStream* eventStream) {
+#ifndef Q_WS_MAC
+	setWindowIcon(QIcon(":/logo-icon-16.png"));
+#endif
+	eventStream_ = eventStream;
+	setupUi(this);
+	model_ = new UserSearchModel();
+	delegate_ = new UserSearchDelegate();
+	results_->setModel(model_);
+	results_->setItemDelegate(delegate_);
+	results_->setHeaderHidden(true);
+#ifdef SWIFT_PLATFORM_MACOSX
+	results_->setAlternatingRowColors(true);
+#endif
+	connect(service_, SIGNAL(activated(const QString&)), this, SLOT(handleGetForm()));
+	connect(getSearchForm_, SIGNAL(clicked()), this, SLOT(handleGetForm()));
+	//connect(user_, SIGNAL(returnPressed()), this, SLOT(handleJoin()));
+	//connect(nickName_, SIGNAL(returnPressed()), room_, SLOT(setFocus()));
+	connect(search_, SIGNAL(clicked()), this, SLOT(handleSearch()));
+
+	connect(results_, SIGNAL(clicked(const QModelIndex&)), this, SLOT(handleSelected(const QModelIndex&)));
+	connect(results_, SIGNAL(activated(const QModelIndex&)), this, SLOT(handleActivated(const QModelIndex&)));
+	connect(buttonBox_, SIGNAL(accepted()), this, SLOT(handleOkClicked()));
+	connect(buttonBox_, SIGNAL(rejected()), this, SLOT(handleCancelClicked()));
+	/* When inputs change, check if OK is enabled */
+	connect(jid_, SIGNAL(textChanged(const QString&)), this, SLOT(enableCorrectButtons()));
+	connect(nickName_, SIGNAL(textChanged(const QString&)), this, SLOT(enableCorrectButtons()));
+	connect(startChat_, SIGNAL(stateChanged(int)), this, SLOT(enableCorrectButtons()));
+	connect(addToRoster_, SIGNAL(stateChanged(int)), this, SLOT(enableCorrectButtons()));
+	enableCorrectButtons();
+}
+
+QtUserSearchWindow::~QtUserSearchWindow() {
+
+}
+
+void QtUserSearchWindow::handleGetForm() {
+	lastServiceJID_ = JID(Q2PSTRING(service_->currentText()));
+	onFormRequested(lastServiceJID_);
+}
+
+void QtUserSearchWindow::handleSearch() {
+	boost::shared_ptr<SearchPayload> search(new SearchPayload());
+	if (nickInput_->isEnabled()) {
+		search->setNick(Q2PSTRING(nickInput_->text()));
+	}
+	if (firstInput_->isEnabled()) {
+		search->setFirst(Q2PSTRING(firstInput_->text()));
+	}
+	if (lastInput_->isEnabled()) {
+		search->setLast(Q2PSTRING(lastInput_->text()));
+	}
+	if (emailInput_->isEnabled()) {
+		search->setEMail(Q2PSTRING(emailInput_->text()));
+	}
+	onSearchRequested(search, lastServiceJID_);
+}
+
+void QtUserSearchWindow::show() {
+	clear();
+	enableCorrectButtons();
+	QWidget::show();
+}
+
+void QtUserSearchWindow::enableCorrectButtons() {
+	bool enable = !jid_->text().isEmpty() && (startChat_->isChecked() || (addToRoster_->isChecked() && !nickName_->text().isEmpty()));
+	buttonBox_->button(QDialogButtonBox::Ok)->setEnabled(enable);
+}
+
+void QtUserSearchWindow::handleOkClicked() {
+	JID contact = JID(Q2PSTRING(jid_->text()));
+	String nick = Q2PSTRING(nickName_->text());
+	if (addToRoster_->isChecked()) {
+		boost::shared_ptr<UIEvent> event(new AddContactUIEvent(contact, nick));
+		eventStream_->send(event);
+	}
+	if (startChat_->isChecked()) {
+		boost::shared_ptr<UIEvent> event(new RequestChatUIEvent(contact));
+		eventStream_->send(event);
+	}
+	hide();
+}
+
+void QtUserSearchWindow::handleCancelClicked() {
+	hide();
+}
+
+void QtUserSearchWindow::addSavedServices(const std::vector<JID>& services) {
+	service_->clear();
+	foreach (JID jid, services) {
+		service_->addItem(P2QSTRING(jid.toString()));
+	}
+	service_->clearEditText();
+}
+
+void QtUserSearchWindow::setServerSupportsSearch(bool support) {
+	if (!support) {
+		stack_->setCurrentIndex(0);
+		messageLabel_->setText("This service doesn't support searching for users.");
+		search_->setEnabled(false);
+	}
+}
+
+void QtUserSearchWindow::setSearchFields(boost::shared_ptr<SearchPayload> fields) {
+	bool enabled[8] = {fields->getNick(), fields->getNick(), fields->getFirst(), fields->getFirst(), fields->getLast(), fields->getLast(), fields->getEMail(), fields->getEMail()};
+	QWidget* widget[8] = {nickInputLabel_, nickInput_, firstInputLabel_, firstInput_, lastInputLabel_, lastInput_, emailInputLabel_, emailInput_};
+	for (int i = 0; i < 8; i++) {
+		widget[i]->setVisible(enabled[i]);
+		widget[i]->setEnabled(enabled[i]);
+	}
+	stack_->setCurrentIndex(1);
+	search_->setEnabled(true);
+}
+
+void QtUserSearchWindow::handleActivated(const QModelIndex& index) {
+	if (!index.isValid()) {
+		return;
+	}
+	UserSearchResult* userItem = static_cast<UserSearchResult*>(index.internalPointer());
+	if (userItem) { /* static cast, so always will be, but if we change to be like mucsearch, remember the check.*/
+		handleSelected(index);
+		//handleJoin(); /* Don't do anything automatically on selection.*/
+	}
+}
+
+void QtUserSearchWindow::handleSelected(const QModelIndex& current) {
+	if (!current.isValid()) {
+		return;
+	}
+	UserSearchResult* userItem = static_cast<UserSearchResult*>(current.internalPointer());
+	if (userItem) { /* Remember to leave this if we change to dynamic cast */
+		jid_->setText(P2QSTRING(userItem->getJID().toString()));
+	}
+}
+
+void QtUserSearchWindow::setResults(const std::vector<UserSearchResult>& results) {
+	model_->setResults(results);
+}
+
+void QtUserSearchWindow::setSelectedService(const JID& jid) {
+	service_->setEditText(P2QSTRING(jid.toString()));
+}
+
+void QtUserSearchWindow::clear() {
+	stack_->setCurrentIndex(0);
+	messageLabel_->setText("Please choose a service to search, above");
+	model_->clear();
+	search_->setEnabled(false);
+}
+
+void QtUserSearchWindow::setSearchError(bool error) {
+	//FIXME
+}
+
+}
diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.h b/Swift/QtUI/UserSearch/QtUserSearchWindow.h
new file mode 100644
index 0000000..dca321a
--- /dev/null
+++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swift/QtUI/UserSearch/ui_QtUserSearchWindow.h"
+
+#include "Swift/Controllers/UIInterfaces/UserSearchWindow.h"
+
+
+namespace Swift {
+	class UserSearchModel;
+	class UserSearchDelegate;
+	class UserSearchResult;
+	class UIEventStream;
+	class QtUserSearchWindow : public QWidget, public UserSearchWindow, private Ui::QtUserSearchWindow {
+		Q_OBJECT
+		public:
+			QtUserSearchWindow(UIEventStream* eventStream);
+			virtual ~QtUserSearchWindow();
+
+			virtual void addSavedServices(const std::vector<JID>& services);
+
+			virtual void clear();
+			virtual void show();
+			virtual void setResults(const std::vector<UserSearchResult>& results);
+			virtual void setSelectedService(const JID& jid);
+			virtual void setServerSupportsSearch(bool error);
+			virtual void setSearchError(bool error);
+			virtual void setSearchFields(boost::shared_ptr<SearchPayload> fields) ;
+		private slots:
+			void handleGetForm();
+			void handleSelected(const QModelIndex& current);
+			void handleSearch();
+			void handleActivated(const QModelIndex& index);
+			void handleOkClicked();
+			void handleCancelClicked();
+			void enableCorrectButtons();
+		private:
+			UserSearchModel* model_;
+			UserSearchDelegate* delegate_;
+			UIEventStream* eventStream_;
+			JID lastServiceJID_;
+	};
+}
diff --git a/Swift/QtUI/UserSearch/QtUserSearchWindow.ui b/Swift/QtUI/UserSearch/QtUserSearchWindow.ui
new file mode 100644
index 0000000..56047ce
--- /dev/null
+++ b/Swift/QtUI/UserSearch/QtUserSearchWindow.ui
@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtUserSearchWindow</class>
+ <widget class="QWidget" name="QtUserSearchWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>698</width>
+    <height>569</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Find other users</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <property name="sizeConstraint">
+      <enum>QLayout::SetNoConstraint</enum>
+     </property>
+     <item>
+      <widget class="QFrame" name="frame">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="frameShape">
+        <enum>QFrame::StyledPanel</enum>
+       </property>
+       <property name="frameShadow">
+        <enum>QFrame::Raised</enum>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayout_2">
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Service to search:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="service_">
+          <property name="editable">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_2">
+          <item>
+           <spacer name="horizontalSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="getSearchForm_">
+            <property name="text">
+             <string>Get Search Form</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <widget class="QStackedWidget" name="stack_">
+          <property name="currentIndex">
+           <number>1</number>
+          </property>
+          <widget class="QWidget" name="display">
+           <layout class="QHBoxLayout" name="horizontalLayout_6">
+            <item>
+             <widget class="QLabel" name="messageLabel_">
+              <property name="text">
+               <string>TextLabel</string>
+              </property>
+              <property name="alignment">
+               <set>Qt::AlignHCenter|Qt::AlignTop</set>
+              </property>
+              <property name="wordWrap">
+               <bool>true</bool>
+              </property>
+             </widget>
+            </item>
+           </layout>
+          </widget>
+          <widget class="QWidget" name="legacy">
+           <layout class="QVBoxLayout" name="verticalLayout_4">
+            <item>
+             <layout class="QGridLayout" name="gridLayout">
+              <item row="0" column="0" colspan="2">
+               <widget class="QLabel" name="label_4">
+                <property name="text">
+                 <string>Enter search terms</string>
+                </property>
+               </widget>
+              </item>
+              <item row="1" column="0">
+               <widget class="QLabel" name="nickInputLabel_">
+                <property name="text">
+                 <string>Nickname:</string>
+                </property>
+               </widget>
+              </item>
+              <item row="1" column="1">
+               <widget class="QLineEdit" name="nickInput_"/>
+              </item>
+              <item row="2" column="0">
+               <widget class="QLabel" name="firstInputLabel_">
+                <property name="text">
+                 <string>First name:</string>
+                </property>
+               </widget>
+              </item>
+              <item row="2" column="1">
+               <widget class="QLineEdit" name="firstInput_"/>
+              </item>
+              <item row="3" column="0">
+               <widget class="QLabel" name="lastInputLabel_">
+                <property name="text">
+                 <string>Last name:</string>
+                </property>
+               </widget>
+              </item>
+              <item row="3" column="1">
+               <widget class="QLineEdit" name="lastInput_"/>
+              </item>
+              <item row="4" column="0">
+               <widget class="QLabel" name="emailInputLabel_">
+                <property name="text">
+                 <string>E-Mail:</string>
+                </property>
+               </widget>
+              </item>
+              <item row="4" column="1">
+               <widget class="QLineEdit" name="emailInput_"/>
+              </item>
+             </layout>
+            </item>
+            <item>
+             <spacer name="verticalSpacer">
+              <property name="orientation">
+               <enum>Qt::Vertical</enum>
+              </property>
+              <property name="sizeHint" stdset="0">
+               <size>
+                <width>20</width>
+                <height>40</height>
+               </size>
+              </property>
+             </spacer>
+            </item>
+           </layout>
+          </widget>
+         </widget>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_3">
+          <item>
+           <spacer name="horizontalSpacer_2">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="search_">
+            <property name="text">
+             <string>Search</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item>
+      <widget class="QFrame" name="frame_2">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="frameShape">
+        <enum>QFrame::StyledPanel</enum>
+       </property>
+       <property name="frameShadow">
+        <enum>QFrame::Raised</enum>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayout_3">
+        <item>
+         <widget class="QLabel" name="label_2">
+          <property name="text">
+           <string>Results:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QTreeView" name="results_"/>
+        </item>
+        <item>
+         <widget class="QLabel" name="label_3">
+          <property name="text">
+           <string>Address:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLineEdit" name="jid_"/>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_4">
+          <item>
+           <widget class="QCheckBox" name="addToRoster_">
+            <property name="text">
+             <string>Add to Roster. Nickname:</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QLineEdit" name="nickName_"/>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <layout class="QHBoxLayout" name="horizontalLayout_5">
+          <item>
+           <widget class="QCheckBox" name="startChat_">
+            <property name="text">
+             <string>Start Chat With Contact</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <spacer name="horizontalSpacer_4">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox_">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/Swift/QtUI/UserSearch/UserSearchDelegate.cpp b/Swift/QtUI/UserSearch/UserSearchDelegate.cpp
new file mode 100644
index 0000000..ff3e766
--- /dev/null
+++ b/Swift/QtUI/UserSearch/UserSearchDelegate.cpp
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <QPen>
+#include <QPainter>
+
+#include "Swift/QtUI/UserSearch/UserSearchDelegate.h"
+//#include "Swift/QtUI/Roster/GroupItemDelegate.h"
+//#include "Swift/QtUI/MUCSearch/MUCSearchItem.h"
+//#include "Swift/QtUI/MUCSearch/MUCSearchRoomItem.h"
+//#include "Swift/QtUI/MUCSearch/MUCSearchServiceItem.h"
+
+namespace Swift {
+
+UserSearchDelegate::UserSearchDelegate() {
+
+}
+
+UserSearchDelegate::~UserSearchDelegate() {
+
+}
+
+// QSize MUCSearchDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index ) const {
+// 	// MUCSearchItem* item = static_cast<MUCSearchItem*>(index.internalPointer());
+// 	// if (item && dynamic_cast<MUCSearchMUCItem*>(item)) {
+// 	// 	return mucSizeHint(option, index);
+// 	// } else if (item && dynamic_cast<MUCSearchGroupItem*>(item)) {
+// 	// 	return groupDelegate_->sizeHint(option, index);
+// 	// }
+// 	return QStyledItemDelegate::sizeHint(option, index);
+// }
+
+// QSize MUCSearchDelegate::mucSizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/ ) const {
+// 	QFontMetrics nameMetrics(common_.nameFont);
+// 	QFontMetrics statusMetrics(common_.detailFont);
+// 	int sizeByText = 2 * common_.verticalMargin + nameMetrics.height() + statusMetrics.height();
+// 	return QSize(150, sizeByText);
+// }
+
+// void MUCSearchDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
+// 	MUCSearchItem* item = static_cast<MUCSearchItem*>(index.internalPointer());
+// 	if (item && dynamic_cast<MUCSearchMUCItem*>(item)) {
+// 		paintMUC(painter, option, dynamic_cast<MUCSearchMUCItem*>(item));
+// 	} else if (item && dynamic_cast<MUCSearchGroupItem*>(item)) {
+// 		MUCSearchGroupItem* group = dynamic_cast<MUCSearchGroupItem*>(item);
+// 		groupDelegate_->paint(painter, option, group->data(Qt::DisplayRole).toString(), group->rowCount(), option.state & QStyle::State_Open);
+// 	} else {
+// 		QStyledItemDelegate::paint(painter, option, index);
+// 	}
+// }
+
+// void MUCSearchDelegate::paintMUC(QPainter* painter, const QStyleOptionViewItem& option, MUCSearchMUCItem* item) const {
+// 	painter->save();
+// 	QRect fullRegion(option.rect);
+// 	if ( option.state & QStyle::State_Selected ) {
+// 		painter->fillRect(fullRegion, option.palette.highlight());
+// 		painter->setPen(option.palette.highlightedText().color());
+// 	} else {
+// 		QColor nameColor = item->data(Qt::TextColorRole).value<QColor>();
+// 		painter->setPen(QPen(nameColor));
+// 	}
+
+// 	QFontMetrics nameMetrics(common_.nameFont);
+// 	painter->setFont(common_.nameFont);
+// 	int extraFontWidth = nameMetrics.width("H");
+// 	int leftOffset = common_.horizontalMargin * 2 + extraFontWidth / 2;
+// 	QRect textRegion(fullRegion.adjusted(leftOffset, 0, 0, 0));
+
+// 	int nameHeight = nameMetrics.height() + common_.verticalMargin;
+// 	QRect nameRegion(textRegion.adjusted(0, common_.verticalMargin, 0, 0));
+
+// 	painter->drawText(nameRegion, Qt::AlignTop, item->data(Qt::DisplayRole).toString());
+
+// 	painter->setFont(common_.detailFont);
+// 	painter->setPen(QPen(QColor(160,160,160)));
+
+// 	QRect detailRegion(textRegion.adjusted(0, nameHeight, 0, 0));
+// 	painter->drawText(detailRegion, Qt::AlignTop, item->data(DetailTextRole).toString());
+
+// 	painter->restore();
+// }
+
+}
diff --git a/Swift/QtUI/UserSearch/UserSearchDelegate.h b/Swift/QtUI/UserSearch/UserSearchDelegate.h
new file mode 100644
index 0000000..d046d62
--- /dev/null
+++ b/Swift/QtUI/UserSearch/UserSearchDelegate.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <QStyledItemDelegate>
+
+#include "Swift/QtUI/Roster/DelegateCommons.h"
+
+namespace Swift {
+	class UserSearchDelegate : public QStyledItemDelegate {
+		public:
+			UserSearchDelegate();
+			~UserSearchDelegate();
+	};
+
+}
+
diff --git a/Swift/QtUI/UserSearch/UserSearchModel.cpp b/Swift/QtUI/UserSearch/UserSearchModel.cpp
new file mode 100644
index 0000000..782d2d0
--- /dev/null
+++ b/Swift/QtUI/UserSearch/UserSearchModel.cpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swift/QtUI/UserSearch/UserSearchModel.h"
+
+#include "Swift/QtUI/QtSwiftUtil.h"
+
+namespace Swift {
+
+UserSearchModel::UserSearchModel() {
+}
+
+void UserSearchModel::clear() {
+	emit layoutAboutToBeChanged();
+	results_.clear();
+	emit layoutChanged();
+}
+
+void UserSearchModel::setResults(const std::vector<UserSearchResult>& results) {
+	clear();
+	emit layoutAboutToBeChanged();
+	results_ = results;
+	emit layoutChanged();
+}
+
+int UserSearchModel::columnCount(const QModelIndex& /*parent*/) const {
+	return 1;
+}
+
+QVariant UserSearchModel::data(const QModelIndex& index, int role) const {
+	if (!index.isValid()) return QVariant();
+	UserSearchResult* result = static_cast<UserSearchResult*>(index.internalPointer());
+	switch (role) {
+		case Qt::DisplayRole: return QVariant(P2QSTRING(result->getJID().toString()));
+		default: return QVariant();
+	}
+}
+
+QModelIndex UserSearchModel::index(int row, int column, const QModelIndex & parent) const {
+	if (!hasIndex(row, column, parent)) {
+		return QModelIndex();
+	}
+	return row < (int)results_.size() ? createIndex(row, column, (void*)&(results_[row])) : QModelIndex();
+}
+
+QModelIndex UserSearchModel::parent(const QModelIndex& /*index*/) const {
+	return QModelIndex();
+}
+
+int UserSearchModel::rowCount(const QModelIndex& parentIndex) const {
+	if (!parentIndex.isValid()) {
+		return results_.size();
+	}
+	return 0;
+}
+
+}
diff --git a/Swift/QtUI/UserSearch/UserSearchModel.h b/Swift/QtUI/UserSearch/UserSearchModel.h
new file mode 100644
index 0000000..d766d9a
--- /dev/null
+++ b/Swift/QtUI/UserSearch/UserSearchModel.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+
+#include <QAbstractItemModel>
+#include <QList>
+
+#include "Swift/Controllers/Chat/UserSearchController.h"
+
+namespace Swift {
+	class UserSearchModel : public QAbstractItemModel {
+		Q_OBJECT
+		public:
+			UserSearchModel();
+			void clear();
+			void setResults(const std::vector<UserSearchResult>& results);
+			int columnCount(const QModelIndex& parent = QModelIndex()) const;
+			QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
+			QModelIndex index(int row, int column, const QModelIndex & parent = QModelIndex()) const;
+			QModelIndex parent(const QModelIndex& index) const;
+			int rowCount(const QModelIndex& parent = QModelIndex()) const;
+		private:
+			std::vector<UserSearchResult> results_;
+	};
+
+}
diff --git a/Swiften/Elements/DiscoInfo.cpp b/Swiften/Elements/DiscoInfo.cpp
index 276b341..a939d48 100644
--- a/Swiften/Elements/DiscoInfo.cpp
+++ b/Swiften/Elements/DiscoInfo.cpp
@@ -10,6 +10,7 @@ namespace Swift {
 
 const String DiscoInfo::ChatStatesFeature = String("http://jabber.org/protocol/chatstates");
 const String DiscoInfo::SecurityLabelsFeature = String("urn:xmpp:sec-label:0");
+const String DiscoInfo::JabberSearchFeature = String("jabber:iq:search");
 
 
 bool DiscoInfo::Identity::operator<(const Identity& other) const {
diff --git a/Swiften/Elements/DiscoInfo.h b/Swiften/Elements/DiscoInfo.h
index 038a2f1..41bf6bf 100644
--- a/Swiften/Elements/DiscoInfo.h
+++ b/Swiften/Elements/DiscoInfo.h
@@ -21,6 +21,7 @@ namespace Swift {
 
 			static const String ChatStatesFeature;
 			static const String SecurityLabelsFeature;
+			static const String JabberSearchFeature;
 
 			const static std::string SecurityLabels;
 			class Identity {
diff --git a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
index f57411b..1bbcbf2 100644
--- a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
+++ b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
@@ -39,6 +39,7 @@
 #include "Swiften/Serializer/PayloadSerializers/CommandSerializer.h"
 #include "Swiften/Serializer/PayloadSerializers/InBandRegistrationPayloadSerializer.h"
 #include "Swiften/Serializer/PayloadSerializers/NicknameSerializer.h"
+#include "Swiften/Serializer/PayloadSerializers/SearchPayloadSerializer.h"
 
 namespace Swift {
 
@@ -75,6 +76,7 @@ FullPayloadSerializerCollection::FullPayloadSerializerCollection() {
 	serializers_.push_back(new CommandSerializer());
 	serializers_.push_back(new InBandRegistrationPayloadSerializer());
 	serializers_.push_back(new NicknameSerializer());
+	serializers_.push_back(new SearchPayloadSerializer());
 	foreach(PayloadSerializer* serializer, serializers_) {
 		addSerializer(serializer);
 	}
-- 
cgit v0.10.2-6-g49f6