diff --git a/CMakeLists.txt b/CMakeLists.txt
index f07b70193de66d48a7c8b7ec74f7fd8342dfa4cd..01de678a553003e7bf82512348cd29955d934418 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -76,6 +76,7 @@ set(SRC_FILES
     src/EmojiPanel.cc
     src/EmojiPickButton.cc
     src/EmojiProvider.cc
+    src/ImageItem.cc
     src/TimelineItem.cc
     src/TimelineView.cc
     src/TimelineViewManager.cc
@@ -127,6 +128,7 @@ qt5_wrap_cpp(MOC_HEADERS
     include/EmojiItemDelegate.h
     include/EmojiPanel.h
     include/EmojiPickButton.h
+    include/ImageItem.h
     include/TimelineItem.h
     include/TimelineView.h
     include/TimelineViewManager.h
diff --git a/include/ImageItem.h b/include/ImageItem.h
new file mode 100644
index 0000000000000000000000000000000000000000..7dc8773fd8c900322a1fa78682531f608da27e3b
--- /dev/null
+++ b/include/ImageItem.h
@@ -0,0 +1,73 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TIMELINE_IMAGE_ITEM_H
+#define TIMELINE_IMAGE_ITEM_H
+
+#include <QEvent>
+#include <QMouseEvent>
+#include <QSharedPointer>
+#include <QWidget>
+
+#include "MatrixClient.h"
+
+class ImageItem : public QWidget
+{
+	Q_OBJECT
+public:
+	ImageItem(QSharedPointer<MatrixClient> client,
+		  const Event &event,
+		  const QString &body,
+		  const QUrl &url,
+		  QWidget *parent = nullptr);
+
+	void setImage(const QPixmap &image);
+
+	QSize sizeHint() const override;
+
+protected:
+	void paintEvent(QPaintEvent *event) override;
+	void mousePressEvent(QMouseEvent *event) override;
+	void resizeEvent(QResizeEvent *event) override;
+
+private slots:
+	void imageDownloaded(const QString &event_id, const QPixmap &img);
+
+private:
+	void scaleImage();
+	void openUrl();
+
+	int max_width_ = 500;
+	int max_height_ = 300;
+
+	int width_;
+	int height_;
+
+	QPixmap scaled_image_;
+	QPixmap image_;
+
+	QUrl url_;
+	QString text_;
+
+	int bottom_height_ = 30;
+
+	Event event_;
+
+	QSharedPointer<MatrixClient> client_;
+};
+
+#endif  // TIMELINE_IMAGE_ITEM_H
diff --git a/include/MatrixClient.h b/include/MatrixClient.h
index 8d517b9aa8f10e9f4d033e515e6836daa1a31f3b..ad768eeb2f1ee5bbf18061087c4b2f73fd82e4ed 100644
--- a/include/MatrixClient.h
+++ b/include/MatrixClient.h
@@ -42,6 +42,7 @@ public:
 	void versions() noexcept;
 	void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url);
 	void fetchOwnAvatar(const QUrl &avatar_url);
+	void downloadImage(const QString &event_id, const QUrl &url);
 
 	inline QString getHomeServer();
 	inline int transactionId();
@@ -68,6 +69,7 @@ signals:
 
 	void roomAvatarRetrieved(const QString &roomid, const QPixmap &img);
 	void ownAvatarRetrieved(const QPixmap &img);
+	void imageDownloaded(const QString &event_id, const QPixmap &img);
 
 	// Returned profile data for the user's account.
 	void getOwnProfileResponse(const QUrl &avatar_url, const QString &display_name);
@@ -84,6 +86,7 @@ private:
 		GetOwnProfile,
 		GetOwnAvatar,
 		GetProfile,
+		Image,
 		InitialSync,
 		Login,
 		Logout,
@@ -105,6 +108,7 @@ private:
 	void onInitialSyncResponse(QNetworkReply *reply);
 	void onSyncResponse(QNetworkReply *reply);
 	void onRoomAvatarResponse(QNetworkReply *reply);
+	void onImageResponse(QNetworkReply *reply);
 
 	// Client API prefix.
 	QString api_url_;
diff --git a/include/TimelineItem.h b/include/TimelineItem.h
index 9af025973d0617c0d96117144d5ee939f1585956..626687acab7604fe57e1ae95cce22f0b5cc04cbb 100644
--- a/include/TimelineItem.h
+++ b/include/TimelineItem.h
@@ -23,6 +23,7 @@
 #include <QWidget>
 
 #include "Sync.h"
+#include "ImageItem.h"
 
 class TimelineItem : public QWidget
 {
@@ -35,6 +36,10 @@ public:
 	TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent = 0);
 	TimelineItem(const QString &body, QWidget *parent = 0);
 
+	// For inline images.
+	TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent);
+	TimelineItem(ImageItem *image, const Event &event, QWidget *parent);
+
 	~TimelineItem();
 
 private:
diff --git a/include/TimelineView.h b/include/TimelineView.h
index e1254ff0ef556863a09467947258f3f05234fb01..4400c361d704b3ad7fd41fb6f035507f93bb0830 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -49,11 +49,13 @@ class TimelineView : public QWidget
 	Q_OBJECT
 
 public:
-	explicit TimelineView(QWidget *parent = 0);
-	explicit TimelineView(const QList<Event> &events, QWidget *parent = 0);
+	TimelineView(QSharedPointer<MatrixClient> client, QWidget *parent = 0);
+	TimelineView(const QList<Event> &events, QSharedPointer<MatrixClient> client, QWidget *parent = 0);
 	~TimelineView();
 
+	// FIXME: Reduce the parameters
 	void addHistoryItem(const Event &event, const QString &color, bool with_sender);
+	void addImageItem(const QString &body, const QUrl &url, const Event &event, const QString &color, bool with_sender);
 	int addEvents(const QList<Event> &events);
 	void addUserTextMessage(const QString &msg, int txn_id);
 	void updatePendingMessage(int txn_id, QString event_id);
@@ -76,6 +78,7 @@ private:
 	QString last_sender_;
 
 	QList<PendingMessage> pending_msgs_;
+	QSharedPointer<MatrixClient> client_;
 };
 
 #endif  // HISTORY_VIEW_H
diff --git a/src/ImageItem.cc b/src/ImageItem.cc
new file mode 100644
index 0000000000000000000000000000000000000000..8298926dbf26c5e221dae8c39074c0cbbcc118f8
--- /dev/null
+++ b/src/ImageItem.cc
@@ -0,0 +1,175 @@
+/*
+ * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <QBrush>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QImage>
+#include <QPainter>
+#include <QPixmap>
+
+#include "ImageItem.h"
+
+ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const Event &event, const QString &body, const QUrl &url, QWidget *parent)
+    : QWidget(parent)
+    , url_{url}
+    , text_{body}
+    , event_{event}
+    , client_{client}
+{
+	setMaximumSize(max_width_, max_height_);
+	setMouseTracking(true);
+	setCursor(Qt::PointingHandCursor);
+	setStyleSheet("background-color: blue");
+
+	QList<QString> url_parts = url_.toString().split("mxc://");
+
+	if (url_parts.size() != 2) {
+		qDebug() << "Invalid format for image" << url_.toString();
+		return;
+	}
+
+	QString media_params = url_parts[1];
+	url_ = QString("%1/_matrix/media/r0/download/%2").arg(client_.data()->getHomeServer(), media_params);
+
+	client_.data()->downloadImage(event.eventId(), url_);
+
+	connect(client_.data(),
+		SIGNAL(imageDownloaded(const QString &, const QPixmap &)),
+		this,
+		SLOT(imageDownloaded(const QString &, const QPixmap &)));
+}
+
+void ImageItem::imageDownloaded(const QString &event_id, const QPixmap &img)
+{
+	if (event_id != event_.eventId())
+		return;
+
+	setImage(img);
+}
+
+void ImageItem::openUrl()
+{
+	if (url_.toString().isEmpty())
+		return;
+
+	if (!QDesktopServices::openUrl(url_))
+		qWarning() << "Could not open url" << url_.toString();
+}
+
+void ImageItem::scaleImage()
+{
+	if (image_.isNull())
+		return;
+
+	auto width_ratio = (double)max_width_ / (double)image_.width();
+	auto height_ratio = (double)max_height_ / (double)image_.height();
+
+	auto min_aspect_ratio = std::min(width_ratio, height_ratio);
+
+	if (min_aspect_ratio > 1) {
+		width_ = image_.width();
+		height_ = image_.height();
+	} else {
+		width_ = image_.width() * min_aspect_ratio;
+		height_ = image_.height() * min_aspect_ratio;
+	}
+
+	setMinimumSize(width_, height_);
+	scaled_image_ = image_.scaled(width_, height_, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+}
+
+QSize ImageItem::sizeHint() const
+{
+	if (image_.isNull())
+		return QSize(max_width_, bottom_height_);
+
+	return QSize(width_, height_);
+}
+
+void ImageItem::setImage(const QPixmap &image)
+{
+	image_ = image;
+	scaleImage();
+	update();
+}
+
+void ImageItem::mousePressEvent(QMouseEvent *event)
+{
+	if (event->button() != Qt::LeftButton)
+		return;
+
+	if (image_.isNull()) {
+		openUrl();
+		return;
+	}
+
+	auto point = event->pos();
+
+	// Click on the text box.
+	if (QRect(0, height_ - bottom_height_, width_, bottom_height_).contains(point))
+		openUrl();
+	else
+		qDebug() << "Opening image overlay. Not implemented yet.";
+}
+
+void ImageItem::resizeEvent(QResizeEvent *event)
+{
+	Q_UNUSED(event);
+
+	scaleImage();
+}
+
+void ImageItem::paintEvent(QPaintEvent *event)
+{
+	Q_UNUSED(event);
+
+	QPainter painter(this);
+	painter.setRenderHint(QPainter::Antialiasing);
+
+	QFont font("Open Sans");
+	font.setPixelSize(12);
+
+	QFontMetrics metrics(font);
+	int fontHeight = metrics.height();
+
+	if (image_.isNull()) {
+		int height = fontHeight + 10;
+
+		setMinimumSize(max_width_, fontHeight + 10);
+
+		QString elidedText = metrics.elidedText(text_, Qt::ElideRight, max_width_ - 10);
+
+		painter.setFont(font);
+		painter.setPen(QPen(QColor(66, 133, 244)));
+		painter.drawText(QPoint(0, height / 2 + 2), elidedText);
+
+		return;
+	}
+
+	painter.fillRect(QRect(0, 0, width_, height_), scaled_image_);
+
+	// Bottom text section
+	painter.fillRect(QRect(0, height_ - bottom_height_, width_, bottom_height_),
+			 QBrush(QColor(33, 33, 33, 128)));
+
+	QString elidedText = metrics.elidedText(text_, Qt::ElideRight, width_ - 10);
+
+	painter.setFont(font);
+	painter.setPen(QPen(QColor("white")));
+	painter.drawText(QPoint(5, height_ - fontHeight / 2), elidedText);
+}
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 381f5023ae7374b89a5925494ff56bb5204aee26..6b4a81bba7757a893f195889d0f8ba17c84704c8 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -309,6 +309,30 @@ void MatrixClient::onGetOwnAvatarResponse(QNetworkReply *reply)
 	emit ownAvatarRetrieved(pixmap);
 }
 
+void MatrixClient::onImageResponse(QNetworkReply *reply)
+{
+	reply->deleteLater();
+
+	int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+
+	if (status == 0 || status >= 400) {
+		qWarning() << reply->errorString();
+		return;
+	}
+
+	auto img = reply->readAll();
+
+	if (img.size() == 0)
+		return;
+
+	QPixmap pixmap;
+	pixmap.loadFromData(img);
+
+	auto event_id = reply->property("event_id").toString();
+
+	emit imageDownloaded(event_id, pixmap);
+}
+
 void MatrixClient::onResponse(QNetworkReply *reply)
 {
 	switch (reply->property("endpoint").toInt()) {
@@ -327,6 +351,9 @@ void MatrixClient::onResponse(QNetworkReply *reply)
 	case Endpoint::GetOwnProfile:
 		onGetOwnProfileResponse(reply);
 		break;
+	case Endpoint::Image:
+		onImageResponse(reply);
+		break;
 	case Endpoint::InitialSync:
 		onInitialSyncResponse(reply);
 		break;
@@ -528,6 +555,15 @@ void MatrixClient::fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url
 	reply->setProperty("endpoint", Endpoint::RoomAvatar);
 }
 
+void MatrixClient::downloadImage(const QString &event_id, const QUrl &url)
+{
+	QNetworkRequest image_request(url);
+
+	QNetworkReply *reply = get(image_request);
+	reply->setProperty("event_id", event_id);
+	reply->setProperty("endpoint", Endpoint::Image);
+}
+
 void MatrixClient::fetchOwnAvatar(const QUrl &avatar_url)
 {
 	QList<QString> url_parts = avatar_url.toString().split("mxc://");
diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc
index 607522a350d143aba642904f3b72e84f5c25324b..4d33db7026063768ad9f16cfe155ce459d9245cf 100644
--- a/src/TimelineItem.cc
+++ b/src/TimelineItem.cc
@@ -18,6 +18,7 @@
 #include <QDateTime>
 #include <QDebug>
 
+#include "ImageItem.h"
 #include "TimelineItem.h"
 
 TimelineItem::TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent)
@@ -36,6 +37,42 @@ TimelineItem::TimelineItem(const QString &body, QWidget *parent)
 	setupLayout();
 }
 
+TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent)
+    : QWidget(parent)
+{
+	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
+	generateTimestamp(timestamp);
+	generateBody(event.sender(), color, "");
+
+	top_layout_ = new QHBoxLayout();
+	top_layout_->setMargin(0);
+	top_layout_->addWidget(time_label_);
+
+	auto right_layout = new QVBoxLayout();
+	right_layout->addWidget(content_label_);
+	right_layout->addWidget(image);
+
+	top_layout_->addLayout(right_layout);
+	top_layout_->addStretch(1);
+
+	setLayout(top_layout_);
+}
+
+TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent)
+    : QWidget(parent)
+{
+	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
+	generateTimestamp(timestamp);
+
+	top_layout_ = new QHBoxLayout();
+	top_layout_->setMargin(0);
+	top_layout_->addWidget(time_label_);
+	top_layout_->addWidget(image, 1);
+	top_layout_->addStretch(1);
+
+	setLayout(top_layout_);
+}
+
 TimelineItem::TimelineItem(const Event &event, bool with_sender, const QString &color, QWidget *parent)
     : QWidget(parent)
 {
diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 00a425571638932c214a61df1a84d6b1ce4b6934..95c7a35132c094686c6a9b2c77ea4daa358f4e57 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -21,19 +21,22 @@
 #include <QtWidgets/QLabel>
 #include <QtWidgets/QSpacerItem>
 
+#include "ImageItem.h"
 #include "TimelineItem.h"
 #include "TimelineView.h"
 #include "TimelineViewManager.h"
 
-TimelineView::TimelineView(const QList<Event> &events, QWidget *parent)
+TimelineView::TimelineView(const QList<Event> &events, QSharedPointer<MatrixClient> client, QWidget *parent)
     : QWidget(parent)
+    , client_{client}
 {
 	init();
 	addEvents(events);
 }
 
-TimelineView::TimelineView(QWidget *parent)
+TimelineView::TimelineView(QSharedPointer<MatrixClient> client, QWidget *parent)
     : QWidget(parent)
+    , client_{client}
 {
 	init();
 }
@@ -73,6 +76,28 @@ int TimelineView::addEvents(const QList<Event> &events)
 				addHistoryItem(event, color, with_sender);
 				last_sender_ = event.sender();
 
+				message_count += 1;
+			} else if (msg_type == "m.image") {
+				// TODO: Move this into serialization.
+				if (!event.content().contains("url")) {
+					qWarning() << "Missing url from m.image event" << event.content();
+					continue;
+				}
+
+				if (!event.content().contains("body")) {
+					qWarning() << "Missing body from m.image event" << event.content();
+					continue;
+				}
+
+				QUrl url(event.content().value("url").toString());
+				QString body(event.content().value("body").toString());
+
+				auto with_sender = last_sender_ != event.sender();
+				auto color = TimelineViewManager::getUserColor(event.sender());
+
+				addImageItem(body, url, event, color, with_sender);
+
+				last_sender_ = event.sender();
 				message_count += 1;
 			}
 		}
@@ -111,6 +136,23 @@ void TimelineView::init()
 		SLOT(sliderRangeChanged(int, int)));
 }
 
+void TimelineView::addImageItem(const QString &body,
+				const QUrl &url,
+				const Event &event,
+				const QString &color,
+				bool with_sender)
+{
+	auto image = new ImageItem(client_, event, body, url);
+
+	if (with_sender) {
+		auto item = new TimelineItem(image, event, color, scroll_widget_);
+		scroll_layout_->addWidget(item);
+	} else {
+		auto item = new TimelineItem(image, event, scroll_widget_);
+		scroll_layout_->addWidget(item);
+	}
+}
+
 void TimelineView::addHistoryItem(const Event &event, const QString &color, bool with_sender)
 {
 	TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_);
diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index 3783b2502c5605d28391517beae6d3f343a93d45..ddb142d325cbf314cd7481ec3d4b70b5e296ecfc 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -81,7 +81,7 @@ void TimelineViewManager::initialize(const Rooms &rooms)
 		auto events = it.value().timeline().events();
 
 		// Create a history view with the room events.
-		TimelineView *view = new TimelineView(events);
+		TimelineView *view = new TimelineView(events, client_);
 		views_.insert(it.key(), view);
 
 		// Add the view in the widget stack.