From 09108f5ac3c1a6567730b5bbd0c847f8422ff4a2 Mon Sep 17 00:00:00 2001
From: Richard Maudsley <richard.maudsley@isode.com>
Date: Thu, 2 Jan 2014 12:21:41 +0000
Subject: Improved plaintext chat view for screen-reader support.

Change-Id: Ib7c10350b56683db95f4c6be49f79f8a9d9e80ec

diff --git a/Swift/QtUI/QtChatWindow.cpp b/Swift/QtUI/QtChatWindow.cpp
index d4b1d0f..826ec9e 100644
--- a/Swift/QtUI/QtChatWindow.cpp
+++ b/Swift/QtUI/QtChatWindow.cpp
@@ -109,7 +109,7 @@ QtChatWindow::QtChatWindow(const QString &contact, QtChatTheme* theme, UIEventSt
 	logRosterSplitter_->setAutoFillBackground(true);
 	layout->addWidget(logRosterSplitter_);
 	if (settings_->getSetting(QtUISettingConstants::USE_PLAIN_CHATS) || settings_->getSetting(QtUISettingConstants::USE_SCREENREADER)) {
-		messageLog_ = new QtPlainChatView(this);
+		messageLog_ = new QtPlainChatView(this, eventStream_);
 	}
 	else {
 		messageLog_ = new QtWebKitChatView(this, eventStream_, theme, this); // I accept that passing the ChatWindow in so that the view can call the signals is somewhat inelegant, but it saves a lot of boilerplate. This patch is unpleasant enough already. So let's fix this soon (it at least needs fixing by the time history is sorted), but not now.
diff --git a/Swift/QtUI/QtChatWindow.h b/Swift/QtUI/QtChatWindow.h
index ca0ecad..df1e619 100644
--- a/Swift/QtUI/QtChatWindow.h
+++ b/Swift/QtUI/QtChatWindow.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2010-2013 Kevin Smith
+ * Copyright (c) 2010-2014 Kevin Smith
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -41,9 +41,6 @@ namespace Swift {
 	class QtChatWindowJSBridge;
 	class SettingsProvider;
 
-	// FIXME: Move this to a different file
-	std::string formatSize(const boost::uintmax_t bytes);
-
 	class LabelModel : public QAbstractListModel {
 		Q_OBJECT
 		public:
diff --git a/Swift/QtUI/QtFileTransferListItemModel.cpp b/Swift/QtUI/QtFileTransferListItemModel.cpp
index 00afacb..b9b9fd1 100644
--- a/Swift/QtUI/QtFileTransferListItemModel.cpp
+++ b/Swift/QtUI/QtFileTransferListItemModel.cpp
@@ -9,9 +9,8 @@
 #include <boost/bind.hpp>
 #include <boost/cstdint.hpp>
 
-#include "QtChatWindow.h" // for formatSize
-
 #include <Swiften/Base/boost_bsignals.h>
+#include <Swiften/Base/FileSize.h>
 #include <Swift/Controllers/FileTransfer/FileTransferController.h>
 #include <Swift/Controllers/FileTransfer/FileTransferOverview.h>
 #include "QtSwiftUtil.h"
@@ -109,7 +108,7 @@ int QtFileTransferListItemModel::rowCount(const QModelIndex& /* parent */) const
 }
 
 QModelIndex QtFileTransferListItemModel::index(int row, int column, const QModelIndex& /* parent */) const {
-	return createIndex(row, column, (void*) 0);
+	return createIndex(row, column, static_cast<void*>(0));
 }
 
 }
diff --git a/Swift/QtUI/QtHighlightRulesItemModel.cpp b/Swift/QtUI/QtHighlightRulesItemModel.cpp
index 4efa712..fcbaddc 100644
--- a/Swift/QtUI/QtHighlightRulesItemModel.cpp
+++ b/Swift/QtUI/QtHighlightRulesItemModel.cpp
@@ -206,7 +206,7 @@ bool QtHighlightRulesItemModel::setData(const QModelIndex &index, const QVariant
 			highlightManager_->setRule(index.row(), r);
 			emit dataChanged(index, index);
 			foreach (int column, changedColumns) {
-				QModelIndex i = createIndex(index.row(), column, (void*) 0);
+				QModelIndex i = createIndex(index.row(), column, static_cast<void*>(0));
 				emit dataChanged(i, i);
 			}
 		}
@@ -227,7 +227,7 @@ int QtHighlightRulesItemModel::rowCount(const QModelIndex& /* parent */) const
 
 QModelIndex QtHighlightRulesItemModel::index(int row, int column, const QModelIndex& /* parent */) const
 {
-	return createIndex(row, column, (void*) 0);
+	return createIndex(row, column, static_cast<void*>(0));
 }
 
 bool QtHighlightRulesItemModel::insertRows(int row, int count, const QModelIndex& /* parent */)
diff --git a/Swift/QtUI/QtPlainChatView.cpp b/Swift/QtUI/QtPlainChatView.cpp
index 98d2e8b..ee76438 100644
--- a/Swift/QtUI/QtPlainChatView.cpp
+++ b/Swift/QtUI/QtPlainChatView.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Kevin Smith
+ * Copyright (c) 2013-2014 Kevin Smith and Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -7,29 +7,40 @@
 #include <Swift/QtUI/QtPlainChatView.h>
 
 #include <QTextEdit>
+#include <QScrollBar>
 #include <QVBoxLayout>
+#include <QPushButton>
+#include <QLabel>
+#include <QDialog>
+#include <QProgressBar>
+#include <QFileDialog>
+#include <QInputDialog>
+#include <QMenu>
 
 #include <Swiften/Base/foreach.h>
+#include <Swiften/Base/FileSize.h>
+
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h>
 
 #include <Swift/QtUI/ChatSnippet.h>
 #include <Swift/QtUI/QtSwiftUtil.h>
 #include <Swift/QtUI/QtUtilities.h>
 
-
 namespace Swift {
 
-QtPlainChatView::QtPlainChatView(QWidget* parent) : QtChatView(parent) {
+QtPlainChatView::QtPlainChatView(QtChatWindow *window, UIEventStream* eventStream)
+: QtChatView(window), window_(window), eventStream_(eventStream), idGenerator_(0) {
 	QVBoxLayout* mainLayout = new QVBoxLayout(this);
 	mainLayout->setSpacing(0);
 	mainLayout->setContentsMargins(0,0,0,0);
-	log_ = new QTextEdit(this);
+	log_ = new LogTextEdit(this);
 	log_->setReadOnly(true);
 	log_->setAccessibleName(tr("Chat Messages"));
 	mainLayout->addWidget(log_);
 }
 
 QtPlainChatView::~QtPlainChatView() {
-	
 }
 
 QString chatMessageToString(const ChatWindow::ChatMessage& message) {
@@ -73,8 +84,10 @@ std::string QtPlainChatView::addMessage(const ChatWindow::ChatMessage& message,
 	text += chatMessageToString(message);
 	text += "</p>";
 	log_->append(text);
-	return "";
-};
+	const std::string idx = senderIsSelf ? "" : senderName;
+	lastMessageLabel_[idx] = label;
+	return idx;
+}
 
 std::string QtPlainChatView::addAction(const ChatWindow::ChatMessage& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& /*avatarPath*/, const boost::posix_time::ptime& time, const HighlightAction& /*highlight*/) {
 	QString text = "<p>";
@@ -82,12 +95,319 @@ std::string QtPlainChatView::addAction(const ChatWindow::ChatMessage& message, c
 		text += P2QSTRING(label->getLabel()) + "<br/>";
 	}
 	QString name = senderIsSelf ? "you" : P2QSTRING(senderName);
-	text += QString(tr("At %1 %2 ")).arg(ChatSnippet::timeToEscapedString(B2QDATE(time))).arg(name);
+	text += QString(tr("At %1 <i>%2 ")).arg(ChatSnippet::timeToEscapedString(B2QDATE(time))).arg(name);
+	text += chatMessageToString(message);
+	text += "</i></p>";
+	log_->append(text);
+	const std::string idx = senderIsSelf ? "" : senderName;
+	lastMessageLabel_[idx] = label;
+	return idx;
+}
+
+void QtPlainChatView::addSystemMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction /*direction*/)
+{
+	QString text = "<p><i>";
+	text += chatMessageToString(message);
+	text += "</i></p>";
+	log_->append(text);
+}
+
+void QtPlainChatView::addPresenceMessage(const ChatWindow::ChatMessage& message, ChatWindow::Direction /*direction*/)
+{
+	QString text = "<p><i>";
+	text += chatMessageToString(message);
+	text += "</i></p>";
+	log_->append(text);
+}
+
+void QtPlainChatView::addErrorMessage(const ChatWindow::ChatMessage& message)
+{
+	QString text = "<p><i>";
+	text += chatMessageToString(message);
+	text += "</i></p>";
+	log_->append(text);
+}
+
+void QtPlainChatView::replaceMessage(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& /*highlight*/)
+{
+	QString text = "<p>";
+	if (lastMessageLabel_[id]) {
+		text += P2QSTRING(lastMessageLabel_[id]->getLabel()) + "<br/>";
+	}
+	QString name = id.empty() ? "you" : P2QSTRING(id);
+	text += QString(tr("At %1 %2 corrected the last message to:")).arg(ChatSnippet::timeToEscapedString(B2QDATE(time))).arg(name) + "<br/>";
+	text += chatMessageToString(message);
+	text += "</p>";
+	log_->append(text);
+}
+
+void QtPlainChatView::replaceWithAction(const ChatWindow::ChatMessage& message, const std::string& id, const boost::posix_time::ptime& time, const HighlightAction& /*highlight*/)
+{
+	QString text = "<p>";
+	if (lastMessageLabel_[id]) {
+		text += P2QSTRING(lastMessageLabel_[id]->getLabel()) + "<br/>";
+	}
+	QString name = id.empty() ? "you" : P2QSTRING(id);
+	text += QString(tr("At %1 %2 corrected the last action to: <i>")).arg(ChatSnippet::timeToEscapedString(B2QDATE(time))).arg(name);
+	text += chatMessageToString(message);
+	text += "</i></p>";
+	log_->append(text);
+}
+
+void QtPlainChatView::replaceLastMessage(const ChatWindow::ChatMessage& message)
+{
+	QString text = "<p>The last message was corrected to:<br/>";
 	text += chatMessageToString(message);
 	text += "</p>";
 	log_->append(text);
-	return "";
-};
+}
+
+void QtPlainChatView::setAckState(const std::string& /*id*/, ChatWindow::AckState state)
+{
+	if (state == ChatWindow::Failed) {
+		addSystemMessage(ChatWindow::ChatMessage("Message delivery failed due to disconnection from server."), ChatWindow::DefaultDirection);
+	}
+}
+
+std::string QtPlainChatView::addFileTransfer(const std::string& senderName, bool senderIsSelf, const std::string& filename, const boost::uintmax_t sizeInBytes)
+{
+	const std::string ftId = "ft" + boost::lexical_cast<std::string>(idGenerator_++);
+	const std::string sizeString = formatSize(sizeInBytes);
 
+	FileTransfer* transfer;
+	if (senderIsSelf) {
+		QString description = QInputDialog::getText(this, tr("File transfer description"),
+			tr("Description:"), QLineEdit::Normal, "");
+		/* NOTE: it is not possible to abort if description is not provided, since we must always return a valid transfer id */
+		const std::string message = std::string() + "Confirm file transfer: <i>" + filename + " (" + sizeString + " bytes)</i>";
+		transfer = new FileTransfer(this, senderIsSelf, ftId, filename, ChatWindow::WaitingForAccept, Q2PSTRING(description), message, true);
+		addSystemMessage(ChatWindow::ChatMessage("Preparing to start file transfer..."), ChatWindow::DefaultDirection);
+	} else { /* incoming transfer */
+		const std::string message = std::string() + "Incoming file transfer: <i>" + filename + " (" + sizeString + " bytes)</i>";
+		transfer = new FileTransfer(this, senderIsSelf, ftId, filename, ChatWindow::WaitingForAccept, "", message, true);
+		addSystemMessage("Incoming file transfer from " + senderName + "...", ChatWindow::DefaultDirection);
+	}
+
+	fileTransfers_[ftId] = transfer;
+	layout()->addWidget(transfer->dialog_);
+
+	return ftId;
+}
+
+void QtPlainChatView::setFileTransferProgress(std::string id, const int percentageDone)
+{
+	FileTransferMap::iterator transfer = fileTransfers_.find(id);
+	if (transfer != fileTransfers_.end()) {
+		transfer->second->bar_->setValue(percentageDone);
+	}
+}
+
+void QtPlainChatView::setFileTransferStatus(std::string id, const ChatWindow::FileTransferState state, const std::string& msg)
+{
+	FileTransferMap::iterator transferIter = fileTransfers_.find(id);
+	if (transferIter == fileTransfers_.end()) {
+		return;
+	}
+
+	/* store the layout index so we can restore it to the same location */
+	FileTransfer* oldTransfer = transferIter->second;
+	const int layoutIndex = layout()->indexOf(oldTransfer->dialog_);
+	layout()->removeWidget(oldTransfer->dialog_);
+	const std::string &label = (!msg.empty() ? msg : oldTransfer->message_);
+	FileTransfer* transfer = new FileTransfer(this, oldTransfer->senderIsSelf_, oldTransfer->ftId_, oldTransfer->filename_, state, oldTransfer->description_, label, false);
+	fileTransfers_[oldTransfer->ftId_] = transfer; /* replace the transfer object for this file id */
+	delete oldTransfer;
+
+	/* insert the new dialog at the old position in the layout list */
+	QBoxLayout* parentLayout = dynamic_cast<QBoxLayout*>(layout());
+	assert(parentLayout);
+	parentLayout->insertWidget(layoutIndex, transfer->dialog_);
+
+	/* log the transfer end result as a system message */
+	if (state == ChatWindow::Finished) {
+		addSystemMessage(ChatWindow::ChatMessage("The file transfer completed successfully."), ChatWindow::DefaultDirection);
+	} else if (state == ChatWindow::Canceled) {
+		addSystemMessage(ChatWindow::ChatMessage("The file transfer was canceled."), ChatWindow::DefaultDirection);
+	} else if (state == ChatWindow::FTFailed) {
+		addSystemMessage(ChatWindow::ChatMessage("The file transfer failed."), ChatWindow::DefaultDirection);
+	}
+}
+
+void QtPlainChatView::addMUCInvitation(const std::string& senderName, const JID& jid, const std::string& /*reason*/, const std::string& password, bool /*direct*/, bool isImpromptu, bool isContinuation)
+{
+	PopupDialog *invite = new PopupDialog(this);
+
+	QLabel* statusLabel = new QLabel;
+	std::string msg = senderName + " has invited you to join " + jid.toString() + "...";
+	statusLabel->setText(P2QSTRING(msg));
+	invite->layout_->addWidget(statusLabel);
+	invite->layout_->addWidget(new QLabel); /* padding */
+
+	AcceptMUCInviteAction* accept = new AcceptMUCInviteAction(invite, "Accept", jid, senderName, password, isImpromptu, isContinuation);
+	connect(accept, SIGNAL(clicked()), SLOT(acceptMUCInvite()));
+	invite->layout_->addWidget(accept);
+
+	AcceptMUCInviteAction* reject = new AcceptMUCInviteAction(invite, "Reject", jid, senderName, password, isImpromptu, isContinuation);
+	connect(reject, SIGNAL(clicked()), SLOT(rejectMUCInvite()));
+	invite->layout_->addWidget(reject);
+	statusLabel->setText(P2QSTRING(msg));
+
+	layout()->addWidget(invite->dialog_);
+
+	addSystemMessage(ChatWindow::ChatMessage(msg), ChatWindow::DefaultDirection);
+}
+
+void QtPlainChatView::scrollToBottom()
+{
+	log_->ensureCursorVisible();
+	log_->verticalScrollBar()->setValue(log_->verticalScrollBar()->maximum());
+}
+
+void QtPlainChatView::fileTransferAccept()
+{
+	FileTransfer::Action* action = dynamic_cast<FileTransfer::Action*>(sender());
+	if (!action) {
+		return;
+	}
+
+	FileTransferMap::iterator transferIter = fileTransfers_.find(action->id_);
+	if (transferIter == fileTransfers_.end()) {
+		return;
+	}
+
+	FileTransfer* transfer = transferIter->second;
+
+	if (transfer->senderIsSelf_) { /* if we are the sender, kick of the transfer */
+		window_->onFileTransferStart(transfer->ftId_, transfer->description_);
+	} else { /* ask the user where to save the file first */
+		QString path = QFileDialog::getSaveFileName(this, tr("Save File"), P2QSTRING(transfer->filename_));
+		if (path.isEmpty()) {
+			fileTransferReject(); /* because the user did not select a desintation path */
+			return;
+		}
+		window_->onFileTransferAccept(transfer->ftId_, Q2PSTRING(path));
+	}
+
+	setFileTransferStatus(transfer->ftId_, ChatWindow::Negotiating, transfer->message_);
+}
+
+void QtPlainChatView::fileTransferReject()
+{
+	FileTransfer::Action* action = dynamic_cast<FileTransfer::Action*>(sender());
+	if (action) {
+		window_->onFileTransferCancel(action->id_);
+		fileTransferFinish();
+	}
+}
+
+void QtPlainChatView::fileTransferFinish()
+{
+	FileTransfer::Action* action = dynamic_cast<FileTransfer::Action*>(sender());
+	if (action) {
+		FileTransferMap::iterator transferIter = fileTransfers_.find(action->id_);
+		if (transferIter != fileTransfers_.end()) {
+			delete transferIter->second; /* cause the dialog to close */
+			fileTransfers_.erase(transferIter);
+		}
+	}
+}
+
+void QtPlainChatView::acceptMUCInvite()
+{
+	AcceptMUCInviteAction *action = dynamic_cast<AcceptMUCInviteAction*>(sender());
+	if (action) {
+		eventStream_->send(boost::make_shared<JoinMUCUIEvent>(action->jid_.toString(), action->password_, boost::optional<std::string>(), false, false, action->isImpromptu_, action->isContinuation_));
+		delete action->parent_;
+	}
+}
+
+void QtPlainChatView::rejectMUCInvite()
+{
+	AcceptMUCInviteAction *action = dynamic_cast<AcceptMUCInviteAction*>(sender());
+	if (action) {
+		/* NOTE: no action required to reject an invite? */
+		delete action->parent_;
+	}
+}
+
+QtPlainChatView::FileTransfer::FileTransfer(QtPlainChatView* parent, bool senderIsSelf, const std::string& ftId, const std::string& filename, const ChatWindow::FileTransferState state, const std::string &desc, const std::string& msg, bool initializing)
+: PopupDialog(parent), bar_(0), senderIsSelf_(senderIsSelf), ftId_(ftId), filename_(filename), description_(desc), message_(msg), initializing_(initializing)
+{
+	QHBoxLayout* layout = new QHBoxLayout;
+	QLabel* statusLabel = new QLabel;
+	layout_->addWidget(statusLabel);
+	layout_->addWidget(new QLabel); /* padding */
+
+	if (initializing_) {
+		FileTransfer::Action* accept = new FileTransfer::Action(senderIsSelf?"Confirm":"Accept", ftId);
+		parent->connect(accept, SIGNAL(clicked()), SLOT(fileTransferAccept()));
+		layout_->addWidget(accept);
+		FileTransfer::Action* reject = new FileTransfer::Action(senderIsSelf?"Cancel":"Reject", ftId);
+		parent->connect(reject, SIGNAL(clicked()), SLOT(fileTransferReject()));
+		layout_->addWidget(reject);
+		statusLabel->setText(P2QSTRING(msg));
+		return;
+	}
+
+	std::string status = msg;
+
+	switch (state) {
+		case ChatWindow::WaitingForAccept: {
+			status = "Waiting for user to accept <i>" + filename + "</i>...";
+			FileTransfer::Action* cancel = new FileTransfer::Action("Cancel", ftId);
+			parent->connect(cancel, SIGNAL(clicked()), SLOT(fileTransferReject()));
+			layout_->addWidget(cancel);
+			break;
+		}
+		case ChatWindow::Negotiating: {
+			status = "Preparing to transfer <i>" + filename + "</i>...";
+			FileTransfer::Action* cancel = new FileTransfer::Action("Cancel", ftId);
+			parent->connect(cancel, SIGNAL(clicked()), SLOT(fileTransferReject()));
+			layout_->addWidget(cancel);
+			break;
+		}
+		case ChatWindow::Transferring: {
+			status = "Transferring <i>" + filename + "</i>...";
+			bar_ = new QProgressBar;
+			bar_->setRange(0, 100);
+			bar_->setValue(0);
+			layout->addWidget(bar_);
+			FileTransfer::Action* cancel = new FileTransfer::Action("Cancel", ftId);
+			parent->connect(cancel, SIGNAL(clicked()), SLOT(fileTransferReject()));
+			layout_->addWidget(cancel);
+			break;
+		}
+		case ChatWindow::Canceled: {
+			status = "File <i>" + filename + "</i> was canceled.";
+			FileTransfer::Action* finish = new FileTransfer::Action("Hide", ftId);
+			parent->connect(finish, SIGNAL(clicked()), SLOT(fileTransferFinish()));
+			layout_->addWidget(finish);
+			break;
+		}
+		case ChatWindow::Finished: {
+			status = "File <i>" + filename + "</i> was transfered successfully.";
+			FileTransfer::Action* finish = new FileTransfer::Action("Hide", ftId);
+			parent->connect(finish, SIGNAL(clicked()), SLOT(fileTransferFinish()));
+			layout_->addWidget(finish);
+			break;
+		}
+		case ChatWindow::FTFailed: {
+			status = "File transfer failed: <i>" + filename + "</i>";
+			FileTransfer::Action* finish = new FileTransfer::Action("Hide", ftId);
+			parent->connect(finish, SIGNAL(clicked()), SLOT(fileTransferFinish()));
+			layout_->addWidget(finish);
+			break;
+		}
+	}
+
+	statusLabel->setText(P2QSTRING(status));
+}
+
+void QtPlainChatView::LogTextEdit::contextMenuEvent(QContextMenuEvent *event)
+{
+	QMenu *menu = createStandardContextMenu();
+	menu->exec(event->globalPos());
+	delete menu;
+}
 
 }
diff --git a/Swift/QtUI/QtPlainChatView.h b/Swift/QtUI/QtPlainChatView.h
index c475862..01b5925 100644
--- a/Swift/QtUI/QtPlainChatView.h
+++ b/Swift/QtUI/QtPlainChatView.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013 Kevin Smith
+ * Copyright (c) 2013-2014 Kevin Smith and Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
@@ -11,12 +11,15 @@
 #include <boost/date_time/posix_time/posix_time.hpp>
 
 #include <QWidget>
+#include <QTextEdit>
 
 #include <Swift/Controllers/UIInterfaces/ChatWindow.h>
 
 #include <Swift/QtUI/QtChatView.h>
+#include <Swift/QtUI/QtChatWindow.h>
 
 class QTextEdit;
+class QProgressBar;
 
 namespace Swift {
 	class HighlightAction;
@@ -25,7 +28,7 @@ namespace Swift {
 	class QtPlainChatView : public QtChatView {
 		Q_OBJECT
 		public:
-			QtPlainChatView(QWidget* parent);
+			QtPlainChatView(QtChatWindow *window, UIEventStream* eventStream);
 			virtual ~QtPlainChatView();
 
 			/** Add message to window.
@@ -37,19 +40,19 @@ namespace Swift {
 			 */
 			virtual std::string addAction(const ChatWindow::ChatMessage& /*message*/, const std::string& /*senderName*/, bool /*senderIsSelf*/, boost::shared_ptr<SecurityLabel> /*label*/, const std::string& /*avatarPath*/, const boost::posix_time::ptime& /*time*/, const HighlightAction& /*highlight*/);
 
-			virtual void addSystemMessage(const ChatWindow::ChatMessage& /*message*/, ChatWindow::Direction /*direction*/) {};
-			virtual void addPresenceMessage(const ChatWindow::ChatMessage& /*message*/, ChatWindow::Direction /*direction*/) {};
-
-			virtual void addErrorMessage(const ChatWindow::ChatMessage& /*message*/) {};
-			virtual void replaceMessage(const ChatWindow::ChatMessage& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/, const HighlightAction& /*highlight*/) {};
-			virtual void replaceWithAction(const ChatWindow::ChatMessage& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/, const HighlightAction& /*highlight*/) {};
-			virtual void replaceLastMessage(const ChatWindow::ChatMessage& /*message*/) {};
-			virtual void setAckState(const std::string& /*id*/, ChatWindow::AckState /*state*/) {};
-			
-			virtual std::string addFileTransfer(const std::string& /*senderName*/, bool /*senderIsSelf*/, const std::string& /*filename*/, const boost::uintmax_t /*sizeInBytes*/) {return "";};
-			virtual void setFileTransferProgress(std::string, const int /*percentageDone*/) {};
-			virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState /*state*/, const std::string& /*msg*/ = "") {};
-			virtual void addMUCInvitation(const std::string& /*senderName*/, const JID& /*jid*/, const std::string& /*reason*/, const std::string& /*password*/, bool /*direct*/, bool /*isImpromptu*/, bool /*isContinuation*/) {};
+			virtual void addSystemMessage(const ChatWindow::ChatMessage& /*message*/, ChatWindow::Direction /*direction*/);
+			virtual void addPresenceMessage(const ChatWindow::ChatMessage& /*message*/, ChatWindow::Direction /*direction*/);
+			virtual void addErrorMessage(const ChatWindow::ChatMessage& /*message*/);
+
+			virtual void replaceMessage(const ChatWindow::ChatMessage& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/, const HighlightAction& /*highlight*/);
+			virtual void replaceWithAction(const ChatWindow::ChatMessage& /*message*/, const std::string& /*id*/, const boost::posix_time::ptime& /*time*/, const HighlightAction& /*highlight*/);
+			virtual void replaceLastMessage(const ChatWindow::ChatMessage& /*message*/);
+			virtual void setAckState(const std::string& /*id*/, ChatWindow::AckState /*state*/);
+
+			virtual std::string addFileTransfer(const std::string& /*senderName*/, bool /*senderIsSelf*/, const std::string& /*filename*/, const boost::uintmax_t /*sizeInBytes*/);
+			virtual void setFileTransferProgress(std::string, const int /*percentageDone*/);
+			virtual void setFileTransferStatus(std::string, const ChatWindow::FileTransferState /*state*/, const std::string& /*msg*/ = "");
+			virtual void addMUCInvitation(const std::string& /*senderName*/, const JID& /*jid*/, const std::string& /*reason*/, const std::string& /*password*/, bool /*direct*/, bool /*isImpromptu*/, bool /*isContinuation*/);
 			virtual std::string addWhiteboardRequest(const QString& /*contact*/, bool /*senderIsSelf*/) {return "";};
 			virtual void setWhiteboardSessionStatus(const std::string& /*id*/, const ChatWindow::WhiteboardSessionState /*state*/) {};
 			virtual void setMessageReceiptState(const std::string& /*id*/, ChatWindow::ReceiptState /*state*/) {};
@@ -59,11 +62,72 @@ namespace Swift {
 
 		public slots:
 			virtual void resizeFont(int /*fontSizeSteps*/) {};
-			virtual void scrollToBottom() {};
+			virtual void scrollToBottom();
 			virtual void handleKeyPressEvent(QKeyEvent* /*event*/) {};
+			virtual void fileTransferAccept();
+			virtual void fileTransferReject();
+			virtual void fileTransferFinish();
+			virtual void acceptMUCInvite();
+			virtual void rejectMUCInvite();
 
 		private:
-			QTextEdit* log_;
+			struct PopupDialog {
+				PopupDialog(QtPlainChatView* parent) {
+					dialog_ = new QFrame(parent);
+					dialog_->setFrameShape(QFrame::Panel);
+					dialog_->setFrameShadow(QFrame::Raised);
+					layout_ = new QHBoxLayout;
+					dialog_->setLayout(layout_);
+				}
+				virtual ~PopupDialog() {
+					delete dialog_;
+				}
+				QFrame* dialog_;
+				QHBoxLayout* layout_;
+			};
+
+			struct AcceptMUCInviteAction : public QPushButton {
+				AcceptMUCInviteAction(PopupDialog* parent, const std::string& text, const JID& jid, const std::string& senderName, const std::string& password, bool isImpromptu, bool isContinuation)
+				: QPushButton(P2QSTRING(text)), parent_(parent), jid_(jid), senderName_(senderName), password_(password), isImpromptu_(isImpromptu), isContinuation_(isContinuation) {}
+				PopupDialog *parent_;
+				JID jid_;
+				std::string senderName_;
+				std::string password_;
+				bool isImpromptu_;
+				bool isContinuation_;
+			};
+
+			struct FileTransfer : public PopupDialog {
+				struct Action : QPushButton {
+					Action(const std::string& text, const std::string& id)
+					: QPushButton(P2QSTRING(text)), id_(id) {}
+					std::string id_;
+				};
+				FileTransfer(QtPlainChatView* parent, bool senderIsSelf, const std::string& ftId, const std::string& filename, const ChatWindow::FileTransferState state, const std::string& desc, const std::string& msg, bool initializing);
+				QProgressBar* bar_;
+				bool senderIsSelf_;
+				std::string ftId_;
+				std::string filename_;
+				std::string description_;
+				std::string message_;
+				bool initializing_;
+			};
+
+			class LogTextEdit : public QTextEdit {
+			public:
+				LogTextEdit(QWidget* parent) : QTextEdit(parent) {}
+				virtual ~LogTextEdit() {}
+				virtual void contextMenuEvent(QContextMenuEvent *event);
+			};
+
+			typedef std::map<std::string, FileTransfer*> FileTransferMap;
+			QtChatWindow* window_;
+			UIEventStream* eventStream_;
+			LogTextEdit* log_;
+			QMenu* logMenu_;
+			FileTransferMap fileTransfers_;
+			std::map<std::string, boost::shared_ptr<SecurityLabel> > lastMessageLabel_;
+			int idGenerator_;
 
 	};
 }
diff --git a/Swift/QtUI/QtWebKitChatView.cpp b/Swift/QtUI/QtWebKitChatView.cpp
index af21609..d1e250c 100644
--- a/Swift/QtUI/QtWebKitChatView.cpp
+++ b/Swift/QtUI/QtWebKitChatView.cpp
@@ -1,13 +1,11 @@
 /*
- * Copyright (c) 2010-2013 Remko Tronçon
+ * Copyright (c) 2010-2014 Remko Tronçon
  * Licensed under the GNU General Public License v3.
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
 #include "QtWebKitChatView.h"
 
-#include <boost/format.hpp>
-
 #include <QtDebug>
 #include <QEventLoop>
 #include <QFile>
@@ -23,6 +21,7 @@
 #include <QFileDialog>
 
 #include <Swiften/Base/Log.h>
+#include <Swiften/Base/FileSize.h>
 #include <Swiften/StringCodecs/Base64.h>
 
 #include <Swift/Controllers/UIEvents/UIEventStream.h>
@@ -621,18 +620,6 @@ std::string QtWebKitChatView::addAction(const ChatWindow::ChatMessage& message,
 	return addMessage(" *" + chatMessageToHTML(message) + "*", senderName, senderIsSelf, label, avatarPath, "font-style:italic ", time, highlight, ChatSnippet::getDirection(message));
 }
 
-// FIXME: Move this to a different file
-std::string formatSize(const boost::uintmax_t bytes) {
-	static const char *siPrefix[] = {"k", "M", "G", "T", "P", "E", "Z", "Y", NULL};
-	int power = 0;
-	double engBytes = bytes;
-	while (engBytes >= 1000) {
-		++power;
-		engBytes = engBytes / 1000.0;
-	}
-	return str( boost::format("%.1lf %sB") % engBytes % (power > 0 ? siPrefix[power-1] : "") );
-}
-
 static QString encodeButtonArgument(const QString& str) {
 	return QtUtilities::htmlEscape(P2QSTRING(Base64::encode(createByteArray(Q2PSTRING(str)))));
 }
diff --git a/Swiften/Base/FileSize.cpp b/Swiften/Base/FileSize.cpp
new file mode 100644
index 0000000..90fdc9a
--- /dev/null
+++ b/Swiften/Base/FileSize.cpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2010-2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swiften/Base/FileSize.h>
+#include <boost/format.hpp>
+
+namespace Swift {
+
+std::string formatSize(const boost::uintmax_t bytes) {
+	static const char *siPrefix[] = {"k", "M", "G", "T", "P", "E", "Z", "Y", NULL};
+	int power = 0;
+	double engBytes = bytes;
+	while (engBytes >= 1000) {
+		++power;
+		engBytes = engBytes / 1000.0;
+	}
+	return str( boost::format("%.1lf %sB") % engBytes % (power > 0 ? siPrefix[power-1] : "") );
+}
+
+}
diff --git a/Swiften/Base/FileSize.h b/Swiften/Base/FileSize.h
new file mode 100644
index 0000000..c9ed5fe
--- /dev/null
+++ b/Swiften/Base/FileSize.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2010-2014 Kevin Smith and Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Base/API.h>
+#include <boost/cstdint.hpp>
+#include <string>
+
+namespace Swift {
+
+SWIFTEN_API std::string formatSize(const boost::uintmax_t bytes);
+
+}
diff --git a/Swiften/Base/SConscript b/Swiften/Base/SConscript
index 094059a..d7fd7cc 100644
--- a/Swiften/Base/SConscript
+++ b/Swiften/Base/SConscript
@@ -16,6 +16,7 @@ objects = swiften_env.SwiftenObject([
 			"BoostRandomGenerator.cpp",
 			"sleep.cpp",
 			"URL.cpp",
-			"Regex.cpp"
+			"Regex.cpp",
+			"FileSize.cpp"
 		])
 swiften_env.Append(SWIFTEN_OBJECTS = [objects])
-- 
cgit v0.10.2-6-g49f6