From 836925a5cdc7017da7fb84416c803e652b48e399 Mon Sep 17 00:00:00 2001
From: Tobias Markmann <tm@ayena.de>
Date: Tue, 26 Feb 2013 00:20:59 +0100
Subject: Adding support for idle time.

Change-Id: I1b14edb97a0c87431ec377b084362e9761caded9
License: This patch is BSD-licensed, see Documentation/Licenses/BSD-simplified.txt for details.

diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp
index edd2e3b..2d2d941 100644
--- a/Swift/Controllers/Chat/ChatController.cpp
+++ b/Swift/Controllers/Chat/ChatController.cpp
@@ -31,6 +31,7 @@
 #include <Swift/Controllers/UIEvents/ShowWhiteboardUIEvent.h>
 #include <Swiften/Elements/DeliveryReceipt.h>
 #include <Swiften/Elements/DeliveryReceiptRequest.h>
+#include <Swiften/Elements/Idle.h>
 #include <Swift/Controllers/SettingConstants.h>
 #include <Swift/Controllers/Highlighter.h>
 #include <Swiften/Base/Log.h>
@@ -62,6 +63,10 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ
 		startMessage = str(format(QT_TRANSLATE_NOOP("", "Starting chat with %1% - %2%")) % nick % contact.toBare().toString());
 		theirPresence = contact.isBare() ? presenceOracle->getHighestPriorityPresence(contact.toBare()) : presenceOracle->getLastPresence(contact);
 	}
+	Idle::ref idle;
+	if (theirPresence && (idle = theirPresence->getPayload<Idle>())) {
+		startMessage += QT_TRANSLATE_NOOP("", ", who has been idle since ") + boost::posix_time::to_simple_string(idle->getSince());
+	}
 	startMessage += ": " + statusShowTypeToFriendlyName(theirPresence ? theirPresence->getShow() : StatusShow::None);
 	if (theirPresence && !theirPresence->getStatus().empty()) {
 		startMessage += " (" + theirPresence->getStatus() + ")";
@@ -335,6 +340,11 @@ std::string ChatController::getStatusChangeString(boost::shared_ptr<Presence> pr
 			response = QT_TRANSLATE_NOOP("", "%1% is now busy");
 		} 
 	}
+	Idle::ref idle;
+	if ((idle = presence->getPayload<Idle>())) {
+		response += QT_TRANSLATE_NOOP("", " and has been idle since ") + boost::posix_time::to_simple_string(idle->getSince());
+	}
+
 	if (!response.empty()) {
 		response = str(format(response) % nick);
 	}
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 87ec94d..0c9c09c 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -461,7 +461,7 @@ void MainController::handleInputIdleChanged(bool idle) {
 	}
 	else {
 		if (idle) {
-			if (statusTracker_->goAutoAway()) {
+			if (statusTracker_->goAutoAway(idleDetector_->getIdleTimeSeconds())) {
 				if (client_ && client_->isAvailable()) {
 					sendPresence(statusTracker_->getNextPresence());
 				}
diff --git a/Swift/Controllers/Roster/ContactRosterItem.cpp b/Swift/Controllers/Roster/ContactRosterItem.cpp
index 5b1b6e0..d2edfe7 100644
--- a/Swift/Controllers/Roster/ContactRosterItem.cpp
+++ b/Swift/Controllers/Roster/ContactRosterItem.cpp
@@ -8,6 +8,9 @@
 #include "Swift/Controllers/Roster/GroupRosterItem.h"
 
 #include <Swiften/Base/foreach.h>
+#include <Swiften/Elements/Idle.h>
+
+#include <boost/date_time/posix_time/posix_time.hpp>
 
 namespace Swift {
 
@@ -39,6 +42,15 @@ std::string ContactRosterItem::getStatusText() const {
 	return shownPresence_ ? shownPresence_->getStatus() : "";
 }
 
+std::string ContactRosterItem::getIdleText() const {
+	Idle::ref idle = shownPresence_ ? shownPresence_->getPayload<Idle>() : Idle::ref();
+	if (!idle || idle->getSince().is_not_a_date_time()) {
+		return "";
+	} else {
+		return boost::posix_time::to_simple_string(idle->getSince());
+	}
+}
+
 void ContactRosterItem::setAvatarPath(const std::string& path) {
 	avatarPath_ = path;
 	onDataChanged();
diff --git a/Swift/Controllers/Roster/ContactRosterItem.h b/Swift/Controllers/Roster/ContactRosterItem.h
index 8389a44..7a2100e 100644
--- a/Swift/Controllers/Roster/ContactRosterItem.h
+++ b/Swift/Controllers/Roster/ContactRosterItem.h
@@ -17,6 +17,7 @@
 #include <boost/bind.hpp>
 #include "Swiften/Base/boost_bsignals.h"
 #include <boost/shared_ptr.hpp>
+#include <boost/date_time/posix_time/ptime.hpp>
 
 namespace Swift {
 
@@ -35,6 +36,7 @@ class ContactRosterItem : public RosterItem {
 		StatusShow::Type getStatusShow() const;
 		StatusShow::Type getSimplifiedStatusShow() const;
 		std::string getStatusText() const;
+		std::string getIdleText() const;
 		void setAvatarPath(const std::string& path);
 		const std::string& getAvatarPath() const;
 		const JID& getJID() const;
@@ -50,9 +52,11 @@ class ContactRosterItem : public RosterItem {
 		
 		void setSupportedFeatures(const std::set<Feature>& features);
 		bool supportsFeature(Feature feature) const;
+
 	private:
 		JID jid_;
 		JID displayJID_;
+		boost::posix_time::ptime lastAvailableTime_;
 		std::string avatarPath_;
 		std::map<std::string, boost::shared_ptr<Presence> > presences_;
 		boost::shared_ptr<Presence> offlinePresence_;
diff --git a/Swift/Controllers/StatusTracker.cpp b/Swift/Controllers/StatusTracker.cpp
index 0c88f4d..6766c2e 100644
--- a/Swift/Controllers/StatusTracker.cpp
+++ b/Swift/Controllers/StatusTracker.cpp
@@ -8,6 +8,8 @@
 
 #include <boost/smart_ptr/make_shared.hpp>
 
+#include <Swiften/Elements/Idle.h>
+
 namespace Swift {
 
 StatusTracker::StatusTracker() {
@@ -21,6 +23,7 @@ boost::shared_ptr<Presence> StatusTracker::getNextPresence() {
 		presence = boost::make_shared<Presence>();
 		presence->setShow(StatusShow::Away);
 		presence->setStatus(queuedPresence_->getStatus());
+		presence->addPayload(boost::make_shared<Idle>(isAutoAwaySince_));
 	} else {
 		presence = queuedPresence_;
 	}
@@ -35,11 +38,12 @@ void StatusTracker::setRequestedPresence(boost::shared_ptr<Presence> presence) {
 //	}
 }
 
-bool StatusTracker::goAutoAway() {
+bool StatusTracker::goAutoAway(const int& seconds) {
 	if (queuedPresence_->getShow() != StatusShow::Online) {
 		return false;
 	}
 	isAutoAway_ = true;
+	isAutoAwaySince_ = boost::posix_time::second_clock::universal_time() - boost::posix_time::seconds(seconds);
 	return true;
 }
 
diff --git a/Swift/Controllers/StatusTracker.h b/Swift/Controllers/StatusTracker.h
index 4f4e880..10a5c0c 100644
--- a/Swift/Controllers/StatusTracker.h
+++ b/Swift/Controllers/StatusTracker.h
@@ -10,6 +10,8 @@
 
 #include "Swiften/Elements/Presence.h"
 
+#include <boost/date_time/posix_time/posix_time_types.hpp>
+
 namespace Swift {
 
 	class StatusTracker {
@@ -17,10 +19,11 @@ namespace Swift {
 			StatusTracker();
 			boost::shared_ptr<Presence> getNextPresence();
 			void setRequestedPresence(boost::shared_ptr<Presence> presence);
-			bool goAutoAway();
+			bool goAutoAway(const int& seconds);
 			bool goAutoUnAway();
 		private:
 			boost::shared_ptr<Presence> queuedPresence_;
 			bool isAutoAway_;
+			boost::posix_time::ptime isAutoAwaySince_;
 	};
 }
diff --git a/Swift/QtUI/ChatList/ChatListDelegate.cpp b/Swift/QtUI/ChatList/ChatListDelegate.cpp
index 5b879df..5b03ac5 100644
--- a/Swift/QtUI/ChatList/ChatListDelegate.cpp
+++ b/Swift/QtUI/ChatList/ChatListDelegate.cpp
@@ -120,7 +120,7 @@ void ChatListDelegate::paintRecent(QPainter* painter, const QStyleOptionViewItem
 	QString name = item->data(Qt::DisplayRole).toString();
 	//qDebug() << "Avatar for " << name << " = " << avatarPath;
 	QString statusText = item->data(ChatListRecentItem::DetailTextRole).toString();
-	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, item->getChat().unreadCount, compact_);
+	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, false, item->getChat().unreadCount, compact_);
 }
 
 void ChatListDelegate::paintWhiteboard(QPainter* painter, const QStyleOptionViewItem& option, ChatListWhiteboardItem* item) const {
@@ -135,7 +135,8 @@ void ChatListDelegate::paintWhiteboard(QPainter* painter, const QStyleOptionView
 	QString name = item->data(Qt::DisplayRole).toString();
 	//qDebug() << "Avatar for " << name << " = " << avatarPath;
 	QString statusText = item->data(ChatListWhiteboardItem::DetailTextRole).toString();
-	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, item->getChat().unreadCount, compact_);
+	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, false, item->getChat().unreadCount, compact_);
+
 }
 
 }
diff --git a/Swift/QtUI/ChatList/ChatListRecentItem.h b/Swift/QtUI/ChatList/ChatListRecentItem.h
index 4e7bc3e..3f27a68 100644
--- a/Swift/QtUI/ChatList/ChatListRecentItem.h
+++ b/Swift/QtUI/ChatList/ChatListRecentItem.h
@@ -23,7 +23,8 @@ namespace Swift {
 				DetailTextRole = Qt::UserRole,
 				AvatarRole = Qt::UserRole + 1,
 				PresenceIconRole = Qt::UserRole + 2/*,
-				StatusShowTypeRole = Qt::UserRole + 3*/
+				StatusShowTypeRole = Qt::UserRole + 3,
+				IdleRole = Qt::UserRole + 4*/
 			};
 			ChatListRecentItem(const ChatListWindow::Chat& chat, ChatListGroupItem* parent);
 			const ChatListWindow::Chat& getChat() const;
diff --git a/Swift/QtUI/Roster/DelegateCommons.cpp b/Swift/QtUI/Roster/DelegateCommons.cpp
index a575cb0..e7342f3 100644
--- a/Swift/QtUI/Roster/DelegateCommons.cpp
+++ b/Swift/QtUI/Roster/DelegateCommons.cpp
@@ -17,8 +17,8 @@ void DelegateCommons::drawElidedText(QPainter* painter, const QRect& region, con
 	painter->drawText(region, flags, adjustedText.simplified());
 }
 
-void DelegateCommons::paintContact(QPainter* painter, const QStyleOptionViewItem& option, const QColor& nameColor, const QString& avatarPath, const QIcon& presenceIcon, const QString& name, const QString& statusText, int unreadCount, bool compact) const {
-	painter->save();
+void DelegateCommons::paintContact(QPainter* painter, const QStyleOptionViewItem& option, const QColor& nameColor, const QString& avatarPath, const QIcon& presenceIcon, const QString& name, const QString& statusText, bool isIdle, int unreadCount, bool compact) const {
+		painter->save();
 	QRect fullRegion(option.rect);
 	if ( option.state & QStyle::State_Selected ) {
 		painter->fillRect(fullRegion, option.palette.highlight());
@@ -29,6 +29,7 @@ void DelegateCommons::paintContact(QPainter* painter, const QStyleOptionViewItem
 
 	QRect presenceIconRegion(QPoint(farLeftMargin, fullRegion.top()), QSize(presenceIconWidth, fullRegion.height() - verticalMargin));
 
+	QRect idleIconRegion(QPoint(farLeftMargin, fullRegion.top()), QSize(presenceIconWidth*2, fullRegion.height() - verticalMargin));
 	int calculatedAvatarSize = presenceIconRegion.height();
 	//This overlaps the presenceIcon, so must be painted first
 	QRect avatarRegion(QPoint(presenceIconRegion.right() - presenceIconWidth / 2, presenceIconRegion.top()), QSize(calculatedAvatarSize, calculatedAvatarSize));
@@ -51,6 +52,10 @@ void DelegateCommons::paintContact(QPainter* painter, const QStyleOptionViewItem
 	//Paint the presence icon over the top of the avatar
 	presenceIcon.paint(painter, presenceIconRegion, Qt::AlignBottom | Qt::AlignHCenter);
 
+	if (isIdle) {
+		idleIcon.paint(painter, idleIconRegion, Qt::AlignBottom | Qt::AlignHCenter);
+	}
+
 	QFontMetrics nameMetrics(nameFont);
 	painter->setFont(nameFont);
 	int extraFontWidth = nameMetrics.width("H");
diff --git a/Swift/QtUI/Roster/DelegateCommons.h b/Swift/QtUI/Roster/DelegateCommons.h
index 8732598..0684410 100644
--- a/Swift/QtUI/Roster/DelegateCommons.h
+++ b/Swift/QtUI/Roster/DelegateCommons.h
@@ -17,7 +17,7 @@
 namespace Swift {
 	class DelegateCommons {
 		public:
-			DelegateCommons() : nameFont(QApplication::font()), detailFont(QApplication::font()) {
+			DelegateCommons() : nameFont(QApplication::font()), detailFont(QApplication::font()), idleIcon(QIcon(":/icons/zzz.png")) {
 				detailFontSizeDrop = nameFont.pointSize() >= 10 ? 2 : 0;
 				detailFont.setStyle(QFont::StyleItalic);
 				detailFont.setPointSize(nameFont.pointSize() - detailFontSizeDrop);
@@ -26,7 +26,7 @@ namespace Swift {
 			static void drawElidedText(QPainter* painter, const QRect& region, const QString& text, int flags = Qt::AlignTop);
 
 			QSize contactSizeHint(const QStyleOptionViewItem& option, const QModelIndex& index, bool compact) const;
-			void paintContact(QPainter* painter, const QStyleOptionViewItem& option, const QColor& nameColor, const QString& avatarPath, const QIcon& presenceIcon, const QString& name, const QString& statusText, int unreadCount, bool compact) const;
+			void paintContact(QPainter* painter, const QStyleOptionViewItem& option, const QColor& nameColor, const QString& avatarPath, const QIcon& presenceIcon, const QString& name, const QString& statusText, bool isIdle, int unreadCount, bool compact) const;
 
 			int detailFontSizeDrop;
 			QFont nameFont;
@@ -38,5 +38,6 @@ namespace Swift {
 			static const int presenceIconHeight;
 			static const int presenceIconWidth;
 			static const int unreadCountSize;
+			QIcon idleIcon;
 	};
 }
diff --git a/Swift/QtUI/Roster/RosterDelegate.cpp b/Swift/QtUI/Roster/RosterDelegate.cpp
index 5c964ca..aef588c 100644
--- a/Swift/QtUI/Roster/RosterDelegate.cpp
+++ b/Swift/QtUI/Roster/RosterDelegate.cpp
@@ -74,9 +74,10 @@ void RosterDelegate::paintContact(QPainter* painter, const QStyleOptionViewItem&
 	QIcon presenceIcon = index.data(PresenceIconRole).isValid() && !index.data(PresenceIconRole).value<QIcon>().isNull()
 			? index.data(PresenceIconRole).value<QIcon>()
 			: QIcon(":/icons/offline.png");
+	bool isIdle = index.data(IdleRole).isValid() ? index.data(IdleRole).toBool() : false;
 	QString name = index.data(Qt::DisplayRole).toString();
 	QString statusText = index.data(StatusTextRole).toString();
-	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, 0, compact_);
+	common_.paintContact(painter, option, nameColor, avatarPath, presenceIcon, name, statusText, isIdle, 0, compact_);
 }
 
 }
diff --git a/Swift/QtUI/Roster/RosterModel.cpp b/Swift/QtUI/Roster/RosterModel.cpp
index 1fc20dd..885d04c 100644
--- a/Swift/QtUI/Roster/RosterModel.cpp
+++ b/Swift/QtUI/Roster/RosterModel.cpp
@@ -84,7 +84,8 @@ QVariant RosterModel::data(const QModelIndex& index, int role) const {
 		case AvatarRole: return getAvatar(item);
 		case PresenceIconRole: return getPresenceIcon(item);
 		case ChildCountRole: return getChildCount(item);
-	 	default: return QVariant();
+		case IdleRole: return getIsIdle(item);
+		default: return QVariant();
 	}
 }
 
@@ -93,6 +94,11 @@ int RosterModel::getChildCount(RosterItem* item) const {
 	return group ? group->getDisplayedChildren().size() : 0; 
 }
 
+bool RosterModel::getIsIdle(RosterItem* item) const {
+	ContactRosterItem* contact = dynamic_cast<ContactRosterItem*>(item);
+	return contact ? !contact->getIdleText().empty() : false;
+}
+
 QColor RosterModel::intToColor(int color) const {
 	return QColor(
 		((color & 0xFF0000)>>16),
@@ -131,6 +137,9 @@ QString RosterModel::getToolTip(RosterItem* item) const {
 		if (!getStatusText(item).isEmpty()) {
 			tip += ": " + getStatusText(item);
 		}
+		if (!contact->getIdleText().empty()) {
+			tip += "\n " + tr("Idle since ") + P2QSTRING(contact->getIdleText());
+		}
 	}
 	return tip;
 }
diff --git a/Swift/QtUI/Roster/RosterModel.h b/Swift/QtUI/Roster/RosterModel.h
index bd34e9c..23d54f8 100644
--- a/Swift/QtUI/Roster/RosterModel.h
+++ b/Swift/QtUI/Roster/RosterModel.h
@@ -18,6 +18,7 @@ namespace Swift {
 		PresenceIconRole = Qt::UserRole + 2,
 		StatusShowTypeRole = Qt::UserRole + 3,
 		ChildCountRole = Qt::UserRole + 4,
+		IdleRole = Qt::UserRole + 5
 	};
 
 	class QtTreeWidget;
@@ -48,6 +49,7 @@ namespace Swift {
 			QString getStatusText(RosterItem* item) const;
 			QIcon getPresenceIcon(RosterItem* item) const;
 			int getChildCount(RosterItem* item) const;
+			bool getIsIdle(RosterItem* item) const;
 			void reLayout();
 			Roster* roster_;
 			QtTreeWidget* view_;
diff --git a/Swift/QtUI/Swift.qrc b/Swift/QtUI/Swift.qrc
index 934bd80..f1b3140 100644
--- a/Swift/QtUI/Swift.qrc
+++ b/Swift/QtUI/Swift.qrc
@@ -40,5 +40,6 @@
 		<file alias="emoticons/wink.png">../resources/emoticons/wink.png</file>
 		<file alias="icons/star-checked.png">../resources/icons/star-checked2.png</file>
 		<file alias="icons/star-unchecked.png">../resources/icons/star-unchecked2.png</file>
+		<file alias="icons/zzz.png">../resources/icons/zzz.png</file>
 	</qresource>
 </RCC>
diff --git a/Swift/resources/icons/zzz.png b/Swift/resources/icons/zzz.png
new file mode 100644
index 0000000..706c2f4
Binary files /dev/null and b/Swift/resources/icons/zzz.png differ
diff --git a/Swift/resources/icons/zzz.svg b/Swift/resources/icons/zzz.svg
new file mode 100644
index 0000000..adbd0be
--- /dev/null
+++ b/Swift/resources/icons/zzz.svg
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="1024"
+   height="1024"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.48.2 r9819"
+   version="1.0"
+   inkscape:export-filename="/Users/tobias/dev/rep/swift-lastseen/Swift/resources/icons/zzz.png"
+   inkscape:export-xdpi="2.8125"
+   inkscape:export-ydpi="2.8125"
+   sodipodi:docname="Swift_resources_icons_zzz_Before_b244d1f210fa994df40297664f111de31b6bf676.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3155">
+      <stop
+         style="stop-color:#007600;stop-opacity:1;"
+         offset="0"
+         id="stop3157" />
+      <stop
+         style="stop-color:#97f597;stop-opacity:1;"
+         offset="1"
+         id="stop3159" />
+    </linearGradient>
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 526.18109 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="744.09448 : 526.18109 : 1"
+       inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+       id="perspective10" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3155"
+       id="linearGradient3185"
+       gradientUnits="userSpaceOnUse"
+       x1="146.36209"
+       y1="457.4635"
+       x2="376.76218"
+       y2="76.855392"
+       gradientTransform="matrix(0.5616546,0,0,0.5616546,113.6799,171.43482)" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.45254834"
+     inkscape:cx="461.16373"
+     inkscape:cy="517.26047"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:window-width="1440"
+     inkscape:window-height="852"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:snap-global="false"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2988"
+       empspacing="5"
+       visible="true"
+       enabled="true"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,512)">
+    <path
+       style="fill:url(#linearGradient3185);fill-opacity:1;stroke:#004900;stroke-width:40;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;opacity:0"
+       d="m 400.8403,315.01501 c 0,79.25627 -64.77302,144.32061 -143.37683,143.5802 -78.9121,-0.74331 -143.37683,-64.32393 -143.37683,-143.5802 0,-80.79036 64.23282,-234.091516 143.37683,-234.091516 79.14401,0 143.37683,154.835246 143.37683,234.091516 z"
+       id="path2383"
+       sodipodi:nodetypes="csssc"
+       inkscape:connector-curvature="0" />
+    <text
+       xml:space="preserve"
+       style="font-size:145.84281920999998761px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:#ffffff;font-family:Sans;stroke-opacity:1;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none"
+       x="278.41916"
+       y="192.9375"
+       id="text2990"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan2992"
+         x="278.41916"
+         y="192.9375"
+         style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;letter-spacing:-60px;writing-mode:lr-tb;fill:#000000;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Sans;-inkscape-font-specification:Helvetica Bold"><tspan
+           style="font-size:290px;font-weight:bold;letter-spacing:-60px;writing-mode:lr-tb;fill:#000000;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;-inkscape-font-specification:Helvetica Bold;font-family:Sans;font-style:normal;font-stretch:normal;font-variant:normal"
+           id="tspan2998"
+           dy="0"
+           dx="0">Z</tspan><tspan
+           style="font-size:350px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;letter-spacing:-60px;writing-mode:lr-tb;fill:#000000;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Sans;-inkscape-font-specification:Helvetica Bold"
+           id="tspan2994"
+           dy="-60">Z</tspan><tspan
+           style="font-size:450px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;letter-spacing:-60px;writing-mode:lr-tb;fill:#000000;stroke:#ffffff;stroke-width:20;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Sans;-inkscape-font-specification:Helvetica Bold"
+           id="tspan2996"
+           dy="-80">Z</tspan></tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="470.72134"
+       y="-22.449778"
+       id="text3000"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3002"
+         x="470.72134"
+         y="-22.449778" /></text>
+    <text
+       xml:space="preserve"
+       style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="1020.8854"
+       y="840.59418"
+       id="text3791"
+       sodipodi:linespacing="125%"
+       transform="translate(0,-512)"><tspan
+         sodipodi:role="line"
+         id="tspan3793"
+         x="1020.8854"
+         y="840.59418" /></text>
+    <text
+       xml:space="preserve"
+       style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="279.52814"
+       y="289.27185"
+       id="text3766"
+       sodipodi:linespacing="125%"
+       transform="translate(0,-512)"><tspan
+         sodipodi:role="line"
+         id="tspan3768"
+         x="279.52814"
+         y="289.27185" /></text>
+  </g>
+</svg>
diff --git a/Swiften/Elements/Idle.h b/Swiften/Elements/Idle.h
new file mode 100644
index 0000000..572eba2
--- /dev/null
+++ b/Swiften/Elements/Idle.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+#include <boost/date_time/posix_time/posix_time_types.hpp>
+
+#include <Swiften/Elements/Payload.h>
+
+namespace Swift {
+
+	class Idle : public Payload {
+	public:
+		typedef boost::shared_ptr<Idle> ref;
+
+	public:
+		Idle() {}
+		Idle(boost::posix_time::ptime since) : since_(since) {
+		}
+
+		void setSince(boost::posix_time::ptime since) {
+			since_ = since;
+		}
+
+		boost::posix_time::ptime getSince() const {
+			return since_;
+		}
+
+	private:
+		boost::posix_time::ptime since_;
+	};
+
+}
diff --git a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
index a40e8f6..1797e45 100644
--- a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
+++ b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
@@ -67,6 +67,7 @@
 #include <Swiften/Parser/PayloadParsers/DeliveryReceiptParserFactory.h>
 #include <Swiften/Parser/PayloadParsers/DeliveryReceiptRequestParserFactory.h>
 #include <Swiften/Parser/PayloadParsers/WhiteboardParser.h>
+#include <Swiften/Parser/PayloadParsers/IdleParser.h>
 
 using namespace boost;
 
@@ -128,6 +129,7 @@ FullPayloadParserFactoryCollection::FullPayloadParserFactoryCollection() {
 	factories_.push_back(boost::make_shared<GenericPayloadParserFactory<WhiteboardParser> >("wb", "http://swift.im/whiteboard"));
 	factories_.push_back(boost::make_shared<DeliveryReceiptParserFactory>());
 	factories_.push_back(boost::make_shared<DeliveryReceiptRequestParserFactory>());
+	factories_.push_back(boost::make_shared<GenericPayloadParserFactory<IdleParser> >("idle", "urn:xmpp:idle:1"));
 
 	foreach(shared_ptr<PayloadParserFactory> factory, factories_) {
 		addFactory(factory.get());
diff --git a/Swiften/Parser/PayloadParsers/IdleParser.cpp b/Swiften/Parser/PayloadParsers/IdleParser.cpp
new file mode 100644
index 0000000..51aa34b
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/IdleParser.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <Swiften/Parser/PayloadParsers/IdleParser.h>
+
+#include <Swiften/Base/DateTime.h>
+
+namespace Swift {
+
+IdleParser::IdleParser() : level_(0) {
+}
+
+void IdleParser::handleStartElement(const std::string& /*element*/, const std::string& /*ns*/, const AttributeMap& attributes) {
+	if (level_ == 0) {
+		boost::posix_time::ptime since = stringToDateTime(attributes.getAttribute("since"));
+		getPayloadInternal()->setSince(since);
+	}
+	++level_;
+}
+
+void IdleParser::handleEndElement(const std::string&, const std::string&) {
+	--level_;
+}
+
+void IdleParser::handleCharacterData(const std::string&) {
+
+}
+
+}
diff --git a/Swiften/Parser/PayloadParsers/IdleParser.h b/Swiften/Parser/PayloadParsers/IdleParser.h
new file mode 100644
index 0000000..38001b2
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/IdleParser.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Elements/Idle.h>
+#include <Swiften/Parser/GenericPayloadParser.h>
+
+namespace Swift {
+	class IdleParser : public GenericPayloadParser<Idle> {
+		public:
+			IdleParser();
+
+			virtual void handleStartElement(const std::string& element, const std::string&, const AttributeMap& attributes);
+			virtual void handleEndElement(const std::string& element, const std::string&);
+			virtual void handleCharacterData(const std::string& data);
+
+		private:
+			int level_;
+	};
+}
diff --git a/Swiften/Parser/PayloadParsers/UnitTest/IdleParserTest.cpp b/Swiften/Parser/PayloadParsers/UnitTest/IdleParserTest.cpp
new file mode 100644
index 0000000..74da474
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/UnitTest/IdleParserTest.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include <Swiften/Parser/PayloadParsers/UnitTest/PayloadsParserTester.h>
+#include <Swiften/Elements/Presence.h>
+#include <Swiften/Elements/Idle.h>
+#include <Swiften/Base/DateTime.h>
+
+using namespace Swift;
+
+class IdleParserTest : public CppUnit::TestFixture {
+		CPPUNIT_TEST_SUITE(IdleParserTest);
+		CPPUNIT_TEST(testParse_XepWhatever_Example1);
+		CPPUNIT_TEST_SUITE_END();
+
+	public:
+		void testParse_XepWhatever_Example1() {
+			PayloadsParserTester parser;
+			CPPUNIT_ASSERT(parser.parse(
+				"<presence from='juliet@capulet.com/balcony'>\n"
+					"<show>away</show>\n"
+					"<idle xmlns='urn:xmpp:idle:1' since='1969-07-21T02:56:15Z'/>\n"
+				"</presence>\n"
+			));
+
+			Presence::ref presence = parser.getPayload<Presence>();
+			CPPUNIT_ASSERT(presence);
+			Idle::ref idle = presence->getPayload<Idle>();
+			CPPUNIT_ASSERT(idle);
+			CPPUNIT_ASSERT(stringToDateTime("1969-07-21T02:56:15Z") == idle->getSince());
+		}
+};
diff --git a/Swiften/Parser/SConscript b/Swiften/Parser/SConscript
index 64e9eb9..068cbd7 100644
--- a/Swiften/Parser/SConscript
+++ b/Swiften/Parser/SConscript
@@ -69,6 +69,7 @@ sources = [
 		"PayloadParsers/NicknameParser.cpp",
 		"PayloadParsers/ReplaceParser.cpp",
 		"PayloadParsers/LastParser.cpp",
+		"PayloadParsers/IdleParser.cpp",
 		"PayloadParsers/S5BProxyRequestParser.cpp",
 		"PayloadParsers/DeliveryReceiptParser.cpp",
 		"PayloadParsers/DeliveryReceiptRequestParser.cpp",
diff --git a/Swiften/SConscript b/Swiften/SConscript
index 8e6bd97..a62d344 100644
--- a/Swiften/SConscript
+++ b/Swiften/SConscript
@@ -375,6 +375,7 @@ if env["SCONS_STAGE"] == "build" :
 			File("Parser/PayloadParsers/UnitTest/MUCAdminPayloadParserTest.cpp"),
 			File("Parser/PayloadParsers/UnitTest/MUCUserPayloadParserTest.cpp"),
 			File("Parser/PayloadParsers/UnitTest/DeliveryReceiptParserTest.cpp"),
+			File("Parser/PayloadParsers/UnitTest/IdleParserTest.cpp"),
 			File("Parser/UnitTest/BOSHBodyExtractorTest.cpp"),
 			File("Parser/UnitTest/AttributeMapTest.cpp"),
 			File("Parser/UnitTest/IQParserTest.cpp"),
@@ -424,6 +425,7 @@ if env["SCONS_STAGE"] == "build" :
 			File("Serializer/PayloadSerializers/UnitTest/MUCAdminPayloadSerializerTest.cpp"),
 			File("Serializer/PayloadSerializers/UnitTest/JingleSerializersTest.cpp"),
 			File("Serializer/PayloadSerializers/UnitTest/DeliveryReceiptSerializerTest.cpp"),
+			File("Serializer/PayloadSerializers/UnitTest/IdleSerializerTest.cpp"),
 			File("Serializer/UnitTest/StreamFeaturesSerializerTest.cpp"),
 			File("Serializer/UnitTest/AuthSuccessSerializerTest.cpp"),
 			File("Serializer/UnitTest/AuthChallengeSerializerTest.cpp"),
diff --git a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
index b4822cd..3f45a7c 100644
--- a/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
+++ b/Swiften/Serializer/PayloadSerializers/FullPayloadSerializerCollection.cpp
@@ -51,6 +51,7 @@
 #include <Swiften/Serializer/PayloadSerializers/ReplaceSerializer.h>
 #include <Swiften/Serializer/PayloadSerializers/LastSerializer.h>
 #include <Swiften/Serializer/PayloadSerializers/WhiteboardSerializer.h>
+#include <Swiften/Serializer/PayloadSerializers/IdleSerializer.h>
 
 #include <Swiften/Serializer/PayloadSerializers/StreamInitiationFileInfoSerializer.h>
 #include <Swiften/Serializer/PayloadSerializers/JingleContentPayloadSerializer.h>
@@ -110,6 +111,7 @@ FullPayloadSerializerCollection::FullPayloadSerializerCollection() {
 	serializers_.push_back(new ReplaceSerializer());
 	serializers_.push_back(new LastSerializer());
 	serializers_.push_back(new WhiteboardSerializer());
+	serializers_.push_back(new IdleSerializer());
 	
 	serializers_.push_back(new StreamInitiationFileInfoSerializer());
 	serializers_.push_back(new JingleContentPayloadSerializer());
diff --git a/Swiften/Serializer/PayloadSerializers/IdleSerializer.h b/Swiften/Serializer/PayloadSerializers/IdleSerializer.h
new file mode 100644
index 0000000..45f9da4
--- /dev/null
+++ b/Swiften/Serializer/PayloadSerializers/IdleSerializer.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Serializer/GenericPayloadSerializer.h>
+#include <Swiften/Elements/Idle.h>
+#include <Swiften/Base/DateTime.h>
+
+namespace Swift {
+	class IdleSerializer : public GenericPayloadSerializer<Idle> {
+		public:
+			IdleSerializer() : GenericPayloadSerializer<Idle>() {}
+
+			virtual std::string serializePayload(boost::shared_ptr<Idle> idle)  const {
+				return "<idle xmlns='urn:xmpp:idle:1' since='" + dateTimeToString(idle->getSince()) + "'/>";
+			}
+	};
+}
diff --git a/Swiften/Serializer/PayloadSerializers/UnitTest/IdleSerializerTest.cpp b/Swiften/Serializer/PayloadSerializers/UnitTest/IdleSerializerTest.cpp
new file mode 100644
index 0000000..9700869
--- /dev/null
+++ b/Swiften/Serializer/PayloadSerializers/UnitTest/IdleSerializerTest.cpp
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2013 Tobias Markmann
+ * Licensed under the simplified BSD license.
+ * See Documentation/Licenses/BSD-simplified.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+
+#include <boost/make_shared.hpp>
+
+#include <Swiften/Serializer/PayloadSerializers/IdleSerializer.h>
+
+using namespace Swift;
+
+class IdleSerializerTest : public CppUnit::TestFixture {
+		CPPUNIT_TEST_SUITE(IdleSerializerTest);
+		CPPUNIT_TEST(testSerialize);
+		CPPUNIT_TEST_SUITE_END();
+
+	public:
+		IdleSerializerTest() {}
+
+		void testSerialize() {
+			IdleSerializer testling;
+			Idle::ref idle = boost::make_shared<Idle>(stringToDateTime("1969-07-21T02:56:15Z"));
+
+			CPPUNIT_ASSERT_EQUAL(std::string("<idle xmlns='urn:xmpp:idle:1' since='1969-07-21T02:56:15Z'/>"), testling.serialize(idle));
+		}
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(IdleSerializerTest);
-- 
cgit v0.10.2-6-g49f6