From b2f58c4f3eb93e3a32062670df5eb6682aed273a Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Fri, 7 Oct 2011 15:09:27 +0100
Subject: Allow affiliation editing in MUCs.

Resolves: #986
Resolves: #988

diff --git a/.subl/swift.sublime-project b/.subl/swift.sublime-project
index 5746207..1d637c4 100644
--- a/.subl/swift.sublime-project
+++ b/.subl/swift.sublime-project
@@ -5,7 +5,7 @@
 		{
 			"path": "..",
 			"folder_exclude_patterns": ["tmp", ".sconf_temp", ".settings", "Swift.app", "3rdParty"],
-			"file_exclude_patterns": [".cproject", ".project", ".sconsign.dblite", "*~", "config.log", "*.o"]
+			"file_exclude_patterns": ["moc_*", ".cproject", ".project", ".sconsign.dblite", "*~", "config.log", "*.o"]
 		}
 	],
 	"settings":
diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp
index e413d92..30e85ad 100644
--- a/Swift/Controllers/Chat/MUCController.cpp
+++ b/Swift/Controllers/Chat/MUCController.cpp
@@ -77,6 +77,8 @@ MUCController::MUCController (
 	chatWindow_->onConfigurationFormCancelled.connect(boost::bind(&MUCController::handleConfigurationCancelled, this));
 	chatWindow_->onDestroyRequest.connect(boost::bind(&MUCController::handleDestroyRoomRequest, this));
 	chatWindow_->onInvitePersonToThisMUCRequest.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this, _1, _2));
+	chatWindow_->onGetAffiliationsRequest.connect(boost::bind(&MUCController::handleGetAffiliationsRequest, this));
+	chatWindow_->onChangeAffiliationsRequest.connect(boost::bind(&MUCController::handleChangeAffiliationsRequest, this, _1));
 	muc_->onJoinComplete.connect(boost::bind(&MUCController::handleJoinComplete, this, _1));
 	muc_->onJoinFailed.connect(boost::bind(&MUCController::handleJoinFailed, this, _1));
 	muc_->onOccupantJoined.connect(boost::bind(&MUCController::handleOccupantJoined, this, _1));
@@ -86,6 +88,7 @@ MUCController::MUCController (
 	muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1));
 	muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1));
 	muc_->onRoleChangeFailed.connect(boost::bind(&MUCController::handleOccupantRoleChangeFailed, this, _1, _2, _3));
+	muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2));
 	if (timerFactory) {
 		loginCheckTimer_ = boost::shared_ptr<Timer>(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS));
 		loginCheckTimer_->onTick.connect(boost::bind(&MUCController::handleJoinTimeoutTick, this));
@@ -114,6 +117,7 @@ void MUCController::handleWindowOccupantSelectionChanged(ContactRosterItem* item
 	/* FIXME: all of these should be conditional */
 	if (item) {
 		actions.push_back(ChatWindow::Kick);
+		actions.push_back(ChatWindow::Ban);
 		actions.push_back(ChatWindow::MakeModerator);
 		actions.push_back(ChatWindow::MakeParticipant);
 		actions.push_back(ChatWindow::MakeVisitor);
@@ -124,6 +128,7 @@ void MUCController::handleWindowOccupantSelectionChanged(ContactRosterItem* item
 void MUCController::handleActionRequestedOnOccupant(ChatWindow::OccupantAction action, ContactRosterItem* item) {
 	switch (action) {
 		case ChatWindow::Kick: muc_->kickOccupant(item->getJID());break;
+		case ChatWindow::Ban: muc_->changeAffiliation(item->getJID(), MUCOccupant::Outcast);break;
 		case ChatWindow::MakeModerator: muc_->changeOccupantRole(item->getJID(), MUCOccupant::Moderator);break;
 		case ChatWindow::MakeParticipant: muc_->changeOccupantRole(item->getJID(), MUCOccupant::Participant);break;
 		case ChatWindow::MakeVisitor: muc_->changeOccupantRole(item->getJID(), MUCOccupant::Visitor);break;
@@ -201,7 +206,7 @@ void MUCController::handleJoinFailed(boost::shared_ptr<ErrorPayload> error) {
 			break;
 		case ErrorPayload::NotAuthorized: 
 			errorMessage += ": ";
-			errorMessage += QT_TRANSLATE_NOOP("", "A password needed");
+			errorMessage += QT_TRANSLATE_NOOP("", "A password is needed");
 			break;
 		case ErrorPayload::RegistrationRequired: 
 			errorMessage += ": ";
@@ -630,4 +635,23 @@ void MUCController::handleInvitePersonToThisMUCRequest(const JID& jid, const std
 	muc_->invitePerson(jid, reason);
 }
 
+void MUCController::handleGetAffiliationsRequest() {
+	muc_->requestAffiliationList(MUCOccupant::Owner);
+	muc_->requestAffiliationList(MUCOccupant::Admin);
+	muc_->requestAffiliationList(MUCOccupant::Member);
+	muc_->requestAffiliationList(MUCOccupant::Outcast);
+}
+
+typedef std::pair<MUCOccupant::Affiliation, JID> AffiliationChangePair;
+
+void MUCController::handleChangeAffiliationsRequest(const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes) {
+	foreach (const AffiliationChangePair& change, changes) {
+		muc_->changeAffiliation(change.second, change.first);
+	}
+}
+
+void MUCController::handleAffiliationListReceived(MUCOccupant::Affiliation affiliation, const std::vector<JID>& jids) {
+	chatWindow_->setAffiliations(affiliation, jids);
+}
+
 }
diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h
index 19d3f66..16dcb99 100644
--- a/Swift/Controllers/Chat/MUCController.h
+++ b/Swift/Controllers/Chat/MUCController.h
@@ -95,6 +95,9 @@ namespace Swift {
 			void handleInvitePersonToThisMUCRequest(const JID& jid, const std::string& reason);
 			void handleConfigurationCancelled();
 			void handleOccupantRoleChangeFailed(ErrorPayload::ref, const JID&, MUCOccupant::Role);
+			void handleGetAffiliationsRequest();
+			void handleAffiliationListReceived(MUCOccupant::Affiliation affiliation, const std::vector<JID>& jids);
+			void handleChangeAffiliationsRequest(const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes);
 
 		private:
 			MUC::ref muc_;
diff --git a/Swift/Controllers/UIInterfaces/ChatWindow.h b/Swift/Controllers/UIInterfaces/ChatWindow.h
index 4a93b42..6fce7a0 100644
--- a/Swift/Controllers/UIInterfaces/ChatWindow.h
+++ b/Swift/Controllers/UIInterfaces/ChatWindow.h
@@ -17,6 +17,7 @@
 #include <Swiften/Elements/SecurityLabelsCatalog.h>
 #include <Swiften/Elements/ChatState.h>
 #include <Swiften/Elements/Form.h>
+#include <Swiften/Elements/MUCOccupant.h>
 
 
 namespace Swift {
@@ -32,7 +33,7 @@ namespace Swift {
 		public:
 			enum AckState {Pending, Received, Failed};
 			enum Tristate {Yes, No, Maybe};
-			enum OccupantAction {Kick, MakeModerator, MakeParticipant, MakeVisitor};
+			enum OccupantAction {Kick, Ban, MakeModerator, MakeParticipant, MakeVisitor};
 			enum FileTransferState {WaitingForAccept, Negotiating, Transferring, Canceled, Finished, FTFailed};
 			ChatWindow() {}
 			virtual ~ChatWindow() {};
@@ -75,6 +76,7 @@ namespace Swift {
 			virtual void setAckState(const std::string& id, AckState state) = 0;
 			virtual void flash() = 0;
 			virtual void setSubject(const std::string& subject) = 0;
+			virtual void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&) = 0;
 
 			/**
 			 * Set an alert on the window.
@@ -112,6 +114,9 @@ namespace Swift {
 			boost::signal<void ()> onDestroyRequest;
 			boost::signal<void (const JID&, const std::string& /*reason*/)> onInvitePersonToThisMUCRequest;
 			boost::signal<void ()> onConfigurationFormCancelled;
+			boost::signal<void ()> onGetAffiliationsRequest;
+			boost::signal<void (MUCOccupant::Affiliation, const JID&)> onSetAffiliationRequest;
+			boost::signal<void (const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes)> onChangeAffiliationsRequest;
 			
 			// File transfer related
 			boost::signal<void (std::string /* id */)> onFileTransferCancel;
diff --git a/Swift/Controllers/UnitTest/MockChatWindow.h b/Swift/Controllers/UnitTest/MockChatWindow.h
index 7b90a57..7e31179 100644
--- a/Swift/Controllers/UnitTest/MockChatWindow.h
+++ b/Swift/Controllers/UnitTest/MockChatWindow.h
@@ -49,6 +49,7 @@ namespace Swift {
 			void setSubject(const std::string& /*subject*/) {}
 			virtual void showRoomConfigurationForm(Form::ref) {}
 			virtual void addMUCInvitation(const JID& /*jid*/, const std::string& /*reason*/, const std::string& /*password*/) {};
+			virtual void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&) {}
 
 			std::string name_;
 			std::string lastMessageBody_;
diff --git a/Swift/QtUI/QtAffiliationEditor.cpp b/Swift/QtUI/QtAffiliationEditor.cpp
new file mode 100644
index 0000000..ed03c23
--- /dev/null
+++ b/Swift/QtUI/QtAffiliationEditor.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "QtAffiliationEditor.h"
+
+#include <QListWidgetItem>
+#include <QInputDialog>
+
+#include "QtSwiftUtil.h"
+
+
+namespace Swift {
+QtAffiliationEditor::QtAffiliationEditor(QWidget* parent) : QDialog(parent){
+	ui_.setupUi(this);
+	setAttribute(Qt::WA_DeleteOnClose);
+	connect(ui_.affiliation, SIGNAL(currentIndexChanged(int)), this, SLOT(handleCurrentIndexChanged(int)));
+	connect(ui_.addJID, SIGNAL(clicked()), this, SLOT(handleAddClicked()));
+	connect(ui_.removeJID, SIGNAL(clicked()), this, SLOT(handleRemoveClicked()));
+}
+
+QtAffiliationEditor::~QtAffiliationEditor() {
+	
+}
+
+void QtAffiliationEditor::setAffiliations(MUCOccupant::Affiliation affiliation, const std::vector<JID>& jids) {
+	affiliations_[affiliation] = jids;
+	if (affiliationFromIndex(ui_.affiliation->currentIndex()) == affiliation) {
+		handleCurrentIndexChanged(ui_.affiliation->currentIndex());
+	}
+}
+
+const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& QtAffiliationEditor::getChanges() const {
+	return changes_;
+}
+
+void QtAffiliationEditor::handleCurrentIndexChanged(int index) {
+	ui_.list->clear();
+	foreach (const JID& jid, affiliations_[affiliationFromIndex(index)]) {
+		ui_.list->addItem(P2QSTRING(jid.toString()));
+	}
+}
+
+void QtAffiliationEditor::handleAddClicked() {
+	bool ok = false;
+	JID jid = JID(Q2PSTRING(QInputDialog::getText(this, tr("Add User"), tr("Added User's Address:"), QLineEdit::Normal, "", &ok)));
+	if (ok && jid.isValid()) {
+		//FIXME: validation
+		MUCOccupant::Affiliation affiliation = affiliationFromIndex(ui_.affiliation->currentIndex());
+		changes_.push_back(ChangePair(affiliation, jid));
+		ui_.list->addItem(P2QSTRING(jid.toString()));
+		affiliations_[affiliation].push_back(jid);
+	}
+}
+
+void QtAffiliationEditor::handleRemoveClicked() {
+	QListWidgetItem* item = ui_.list->currentItem();
+	if (item) {
+		JID jid(Q2PSTRING(item->text()));
+		changes_.push_back(ChangePair(MUCOccupant::NoAffiliation, jid));
+		std::vector<JID>& jids = affiliations_[affiliationFromIndex(ui_.affiliation->currentIndex())];
+		jids.erase(std::remove(jids.begin(), jids.end(), jid), jids.end());
+		handleCurrentIndexChanged(ui_.affiliation->currentIndex());
+	}
+}
+
+MUCOccupant::Affiliation QtAffiliationEditor::affiliationFromIndex(int affiliation) {
+	switch (affiliation) {
+		case 0: return MUCOccupant::Owner;
+		case 1: return MUCOccupant::Admin;
+		case 2: return MUCOccupant::Member;
+		case 3: return MUCOccupant::Outcast;
+		default: return MUCOccupant::Outcast;
+	}
+}
+
+}
\ No newline at end of file
diff --git a/Swift/QtUI/QtAffiliationEditor.h b/Swift/QtUI/QtAffiliationEditor.h
new file mode 100644
index 0000000..913b2cc
--- /dev/null
+++ b/Swift/QtUI/QtAffiliationEditor.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <vector>
+#include <map>
+
+#include <QDialog>
+#include <Swift/QtUI/ui_QtAffiliationEditor.h>
+
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/MUCOccupant.h>
+
+namespace Swift {
+	class QtAffiliationEditor : public QDialog {
+		Q_OBJECT
+		public:
+			QtAffiliationEditor(QWidget* parent = NULL);
+			~QtAffiliationEditor();
+			void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>& jids);
+			const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& getChanges() const;
+		private slots:
+			void handleCurrentIndexChanged(int);
+			void handleAddClicked();
+			void handleRemoveClicked();
+		private:
+			typedef std::pair<MUCOccupant::Affiliation, JID> ChangePair;
+			MUCOccupant::Affiliation affiliationFromIndex(int affiliation);
+			Ui::QtAffiliationEditor ui_;
+			std::map<MUCOccupant::Affiliation, std::vector<JID> > affiliations_;
+			std::vector<ChangePair> changes_;
+	};
+}
\ No newline at end of file
diff --git a/Swift/QtUI/QtAffiliationEditor.ui b/Swift/QtUI/QtAffiliationEditor.ui
new file mode 100644
index 0000000..1447884
--- /dev/null
+++ b/Swift/QtUI/QtAffiliationEditor.ui
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtAffiliationEditor</class>
+ <widget class="QDialog" name="QtAffiliationEditor">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>575</width>
+    <height>466</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Dialog</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string/>
+     </property>
+     <layout class="QVBoxLayout" name="verticalLayout_3">
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QLabel" name="label">
+          <property name="text">
+           <string>Affiliation:</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="affiliation">
+          <item>
+           <property name="text">
+            <string>Owner</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>Administrator</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>Member</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>Outcast (Banned)</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <item>
+         <widget class="QListWidget" name="list"/>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="verticalLayout">
+          <item>
+           <widget class="QPushButton" name="addJID">
+            <property name="text">
+             <string>Add User</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="removeJID">
+            <property name="text">
+             <string>Remove User</string>
+            </property>
+           </widget>
+          </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>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </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>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>QtAffiliationEditor</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>224</x>
+     <y>444</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>QtAffiliationEditor</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>292</x>
+     <y>450</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index 7e47f4d..0635496 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -53,6 +53,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 	unreadCount_ = 0;
 	inputEnabled_ = true;
 	completer_ = NULL;
+	affiliationEditor_ = NULL;
 	theme_ = theme;
 	isCorrection_ = false;
 	correctionEnabled_ = Maybe;
@@ -165,8 +166,8 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 
 QtChatWindow::~QtChatWindow() {
 	delete fileTransferJS;
-	if (mucConfigurationWindow) {
-		delete mucConfigurationWindow.data();
+	if (mucConfigurationWindow_) {
+		delete mucConfigurationWindow_.data();
 	}
 }
 
@@ -341,7 +342,7 @@ void QtChatWindow::setCorrectionEnabled(Tristate enabled) {
 
 SecurityLabelsCatalog::Item QtChatWindow::getSelectedSecurityLabel() {
 	assert(labelsWidget_->isEnabled());
-	assert(labelsWidget_->currentIndex() >= 0 && labelsWidget_->currentIndex() < availableLabels_.size());
+	assert(labelsWidget_->currentIndex() >= 0 && static_cast<size_t>(labelsWidget_->currentIndex()) < availableLabels_.size());
 	return availableLabels_[labelsWidget_->currentIndex()];
 }
 
@@ -371,8 +372,13 @@ void QtChatWindow::qAppFocusChanged(QWidget* /*old*/, QWidget* /*now*/) {
 
 void QtChatWindow::setInputEnabled(bool enabled) {
 	inputEnabled_ = enabled;
-	if (!enabled && mucConfigurationWindow) {
-		delete mucConfigurationWindow.data();
+	if (!enabled) {
+		if (mucConfigurationWindow_) {
+			delete mucConfigurationWindow_.data();
+		}
+		if (affiliationEditor_) {
+			delete affiliationEditor_.data();
+		}
 	}
 }
 
@@ -704,6 +710,7 @@ void QtChatWindow::handleActionButtonClicked() {
 	QMenu contextMenu;
 	QAction* changeSubject = contextMenu.addAction(tr("Change subject"));
 	QAction* configure = contextMenu.addAction(tr("Configure room"));
+	QAction* affiliations = contextMenu.addAction(tr("Edit affiliations"));
 	QAction* destroy = contextMenu.addAction(tr("Destroy room"));
 	QAction* invite = contextMenu.addAction(tr("Invite person to this room"));
 	QAction* result = contextMenu.exec(QCursor::pos());
@@ -717,6 +724,14 @@ void QtChatWindow::handleActionButtonClicked() {
 	else if (result == configure) {
 		onConfigureRequest(Form::ref());
 	}
+	else if (result == affiliations) {
+		if (!affiliationEditor_) {
+			onGetAffiliationsRequest();
+			affiliationEditor_ = new QtAffiliationEditor(this);
+			connect(affiliationEditor_, SIGNAL(accepted()), this, SLOT(handleAffiliationEditorAccepted()));
+		}
+		affiliationEditor_->show();
+	}
 	else if (result == destroy) {
 		onDestroyRequest();
 	}
@@ -729,13 +744,22 @@ void QtChatWindow::handleActionButtonClicked() {
 	}
 }
 
+void QtChatWindow::handleAffiliationEditorAccepted() {
+	onChangeAffiliationsRequest(affiliationEditor_->getChanges());
+}
+
+void QtChatWindow::setAffiliations(MUCOccupant::Affiliation affiliation, const std::vector<JID>& jids) {
+	if (!affiliationEditor_) return;
+	affiliationEditor_->setAffiliations(affiliation, jids);
+}
+
 void QtChatWindow::showRoomConfigurationForm(Form::ref form) {
-	if (mucConfigurationWindow) {
-		delete mucConfigurationWindow.data();
+	if (mucConfigurationWindow_) {
+		delete mucConfigurationWindow_.data();
 	}
-	mucConfigurationWindow = new QtMUCConfigurationWindow(form);
-	mucConfigurationWindow->onFormComplete.connect(boost::bind(boost::ref(onConfigureRequest), _1));
-	mucConfigurationWindow->onFormCancelled.connect(boost::bind(boost::ref(onConfigurationFormCancelled)));
+	mucConfigurationWindow_ = new QtMUCConfigurationWindow(form);
+	mucConfigurationWindow_->onFormComplete.connect(boost::bind(boost::ref(onConfigureRequest), _1));
+	mucConfigurationWindow_->onFormCancelled.connect(boost::bind(boost::ref(onConfigurationFormCancelled)));
 }
 
 void QtChatWindow::addMUCInvitation(const JID& jid, const std::string& reason, const std::string& password) {
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index 8dfe767..e546f12 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -8,6 +8,7 @@
 
 #include <Swift/Controllers/UIInterfaces/ChatWindow.h>
 #include <Swift/QtUI/QtMUCConfigurationWindow.h>
+#include <Swift/QtUI/QtAffiliationEditor.h>
 
 #include <QtTabbable.h>
 
@@ -73,6 +74,7 @@ namespace Swift {
 			void setSubject(const std::string& subject);
 			void showRoomConfigurationForm(Form::ref);
 			void addMUCInvitation(const JID& jid, const std::string& reason, const std::string& password);
+			void setAffiliations(MUCOccupant::Affiliation, const std::vector<JID>&);
 
 		public slots:
 			void handleChangeSplitterState(QByteArray state);
@@ -111,6 +113,7 @@ namespace Swift {
 			void handleFileTransferSetDescription(QString id);
 			void handleFileTransferStart(QString id);
 			void handleFileTransferAccept(QString id, QString filename);
+			void handleAffiliationEditorAccepted();
 
 		private:
 			void updateTitleWithUnreadCount();
@@ -153,6 +156,7 @@ namespace Swift {
 			QString alertStyleSheet_;
 			std::map<QString, QString> descriptions;
 			QtFileTransferJSBridge* fileTransferJS;
-			QPointer<QtMUCConfigurationWindow> mucConfigurationWindow;
+			QPointer<QtMUCConfigurationWindow> mucConfigurationWindow_;
+			QPointer<QtAffiliationEditor> affiliationEditor_;
 	};
 }
diff --git a/Swift/QtUI/Roster/QtOccupantListWidget.cpp b/Swift/QtUI/Roster/QtOccupantListWidget.cpp
index 3ee0b7d..3f66585 100644
--- a/Swift/QtUI/Roster/QtOccupantListWidget.cpp
+++ b/Swift/QtUI/Roster/QtOccupantListWidget.cpp
@@ -45,6 +45,7 @@ void QtOccupantListWidget::contextMenuEvent(QContextMenuEvent* event) {
 			QString text = "Error: missing string";
 			switch (availableAction) {
 				case ChatWindow::Kick: text = tr("Kick user"); break;
+				case ChatWindow::Ban: text = tr("Kick and ban user"); break;
 				case ChatWindow::MakeModerator: text = tr("Make moderator"); break;
 				case ChatWindow::MakeParticipant: text = tr("Make participant"); break;
 				case ChatWindow::MakeVisitor: text = tr("Remove voice"); break;
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index 82e4490..71dc59f 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -84,8 +84,8 @@ sources = [
     "QtTabWidget.cpp",
     "QtTextEdit.cpp",
     "QtXMLConsoleWidget.cpp",
-	"QtFileTransferListWidget.cpp",
-	"QtFileTransferListItemModel.cpp",
+    "QtFileTransferListWidget.cpp",
+    "QtFileTransferListItemModel.cpp",
     "QtAdHocCommandWindow.cpp",
     "QtUtilities.cpp",
     "QtBookmarkDetailWindow.cpp",
@@ -137,6 +137,7 @@ sources = [
     "qrc_Swift.cc",
     "QtFileTransferJSBridge.cpp",
     "QtMUCConfigurationWindow.cpp",
+    "QtAffiliationEditor.cpp",
   ]
 
 myenv["SWIFT_VERSION"] = Version.getBuildVersion(env.Dir("#").abspath, "swift")
@@ -173,6 +174,7 @@ myenv.Uic4("UserSearch/QtUserSearchFirstPage.ui")
 myenv.Uic4("UserSearch/QtUserSearchFieldsPage.ui")
 myenv.Uic4("UserSearch/QtUserSearchResultsPage.ui")
 myenv.Uic4("QtBookmarkDetailWindow.ui")
+myenv.Uic4("QtAffiliationEditor.ui")
 myenv.Uic4("QtJoinMUCWindow.ui")
 myenv.Qrc("DefaultTheme.qrc")
 myenv.Qrc("Swift.qrc")
diff --git a/Swiften/MUC/MUC.cpp b/Swiften/MUC/MUC.cpp
index 204fdcc..824ced1 100644
--- a/Swiften/MUC/MUC.cpp
+++ b/Swiften/MUC/MUC.cpp
@@ -241,6 +241,9 @@ void MUC::kickOccupant(const JID& jid) {
 	changeOccupantRole(jid, MUCOccupant::NoRole);
 }
 
+/**
+ * Call with the room JID, not the real JID.
+ */
 void MUC::changeOccupantRole(const JID& jid, MUCOccupant::Role role) {
 	MUCAdminPayload::ref mucPayload = boost::make_shared<MUCAdminPayload>();
 	MUCItem item;
@@ -259,6 +262,51 @@ void MUC::handleOccupantRoleChangeResponse(MUCAdminPayload::ref /*unused*/, Erro
 	}
 }
 
+void MUC::requestAffiliationList(MUCOccupant::Affiliation affiliation) {
+	MUCAdminPayload::ref mucPayload = boost::make_shared<MUCAdminPayload>();
+	MUCItem item;
+	item.affiliation = affiliation;
+	mucPayload->addItem(item);
+	GenericRequest<MUCAdminPayload>* request = new GenericRequest<MUCAdminPayload>(IQ::Get, getJID(), mucPayload, iqRouter_);
+	request->onResponse.connect(boost::bind(&MUC::handleAffiliationListResponse, this, _1, _2, affiliation));
+	request->send();
+}
+
+/**
+ * Must be called with the real JID, not the room JID.
+ */
+void MUC::changeAffiliation(const JID& jid, MUCOccupant::Affiliation affiliation) {
+	MUCAdminPayload::ref mucPayload = boost::make_shared<MUCAdminPayload>();
+	MUCItem item;
+	item.affiliation = affiliation;
+	item.realJID = jid;
+	mucPayload->addItem(item);
+	GenericRequest<MUCAdminPayload>* request = new GenericRequest<MUCAdminPayload>(IQ::Set, getJID(), mucPayload, iqRouter_);
+	request->onResponse.connect(boost::bind(&MUC::handleAffiliationChangeResponse, this, _1, _2, jid, affiliation));
+	request->send();
+}
+
+void MUC::handleAffiliationListResponse(MUCAdminPayload::ref payload, ErrorPayload::ref error, MUCOccupant::Affiliation affiliation) {
+	if (error) {
+		onAffiliationListFailed(error);
+	}
+	else {
+		std::vector<JID> jids;
+		foreach (MUCItem item, payload->getItems()) {
+			if (item.realJID) {
+				jids.push_back(*item.realJID);
+			}
+		}
+		onAffiliationListReceived(affiliation, jids);
+	}
+}
+
+void MUC::handleAffiliationChangeResponse(MUCAdminPayload::ref /*unused*/, ErrorPayload::ref error, const JID& jid, MUCOccupant::Affiliation affiliation) {
+	if (error) {
+		onAffiliationChangeFailed(error, jid, affiliation);
+	}
+}
+
 void MUC::changeSubject(const std::string& subject) {
 	Message::ref message = boost::make_shared<Message>();
 	message->setSubject(subject);
diff --git a/Swiften/MUC/MUC.h b/Swiften/MUC/MUC.h
index d855033..1070c69 100644
--- a/Swiften/MUC/MUC.h
+++ b/Swiften/MUC/MUC.h
@@ -58,6 +58,8 @@ namespace Swift {
 			bool hasOccupant(const std::string& nick);
 			void kickOccupant(const JID& jid);
 			void changeOccupantRole(const JID& jid, MUCOccupant::Role role);
+			void requestAffiliationList(MUCOccupant::Affiliation);
+			void changeAffiliation(const JID& jid, MUCOccupant::Affiliation affiliation);
 			void changeSubject(const std::string& subject);
 			void requestConfigurationForm();
 			void configureRoom(Form::ref);
@@ -67,17 +69,21 @@ namespace Swift {
 			void invitePerson(const JID& person, const std::string& reason = "");
 			void setCreateAsReservedIfNew() {createAsReservedIfNew = true;}
 			void setPassword(const boost::optional<std::string>& password);
+			
 		public:
 			boost::signal<void (const std::string& /*nick*/)> onJoinComplete;
 			boost::signal<void (ErrorPayload::ref)> onJoinFailed;
 			boost::signal<void (ErrorPayload::ref, const JID&, MUCOccupant::Role)> onRoleChangeFailed;
+			boost::signal<void (ErrorPayload::ref, const JID&, MUCOccupant::Affiliation)> onAffiliationChangeFailed;
 			boost::signal<void (ErrorPayload::ref)> onConfigurationFailed;
+			boost::signal<void (ErrorPayload::ref)> onAffiliationListFailed;
 			boost::signal<void (Presence::ref)> onOccupantPresenceChange;
 			boost::signal<void (const std::string&, const MUCOccupant& /*now*/, const MUCOccupant::Role& /*old*/)> onOccupantRoleChanged;
 			boost::signal<void (const std::string&, const MUCOccupant::Affiliation& /*new*/, const MUCOccupant::Affiliation& /*old*/)> onOccupantAffiliationChanged;
 			boost::signal<void (const MUCOccupant&)> onOccupantJoined;
 			boost::signal<void (const MUCOccupant&, LeavingType, const std::string& /*reason*/)> onOccupantLeft;
 			boost::signal<void (Form::ref)> onConfigurationFormReceived;
+			boost::signal<void (MUCOccupant::Affiliation, const std::vector<JID>&)> onAffiliationListReceived;
 			/* boost::signal<void (const MUCInfo&)> onInfoResult; */
 			/* boost::signal<void (const blah&)> onItemsResult; */
 			
@@ -96,6 +102,8 @@ namespace Swift {
 			void internalJoin(const std::string& nick);
 			void handleCreationConfigResponse(MUCOwnerPayload::ref, ErrorPayload::ref);
 			void handleOccupantRoleChangeResponse(MUCAdminPayload::ref, ErrorPayload::ref, const JID&, MUCOccupant::Role);
+			void handleAffiliationChangeResponse(MUCAdminPayload::ref, ErrorPayload::ref, const JID&, MUCOccupant::Affiliation);
+			void handleAffiliationListResponse(MUCAdminPayload::ref, ErrorPayload::ref, MUCOccupant::Affiliation);
 			void handleConfigurationFormReceived(MUCOwnerPayload::ref, ErrorPayload::ref);
 			void handleConfigurationResultReceived(MUCOwnerPayload::ref, ErrorPayload::ref);
 
-- 
cgit v0.10.2-6-g49f6