summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt21
-rw-r--r--include/ChatPage.h10
-rw-r--r--include/ImageItem.h10
-rw-r--r--include/RoomInfoListItem.h24
-rw-r--r--include/RoomList.h12
-rw-r--r--include/RoomState.h63
-rw-r--r--include/Sync.h13
-rw-r--r--include/TimelineItem.h17
-rw-r--r--include/TimelineView.h22
-rw-r--r--include/TimelineViewManager.h5
-rw-r--r--include/events/Event.h5
-rw-r--r--include/events/MessageEvent.h67
-rw-r--r--include/events/MessageEventContent.h78
-rw-r--r--include/events/RoomEvent.h5
-rw-r--r--include/events/messages/Audio.h (renamed from src/RoomInfo.cc)70
-rw-r--r--include/events/messages/Emote.h (renamed from include/RoomInfo.h)44
-rw-r--r--include/events/messages/File.h76
-rw-r--r--include/events/messages/Image.h69
-rw-r--r--include/events/messages/Location.h65
-rw-r--r--include/events/messages/Notice.h40
-rw-r--r--include/events/messages/Text.h40
-rw-r--r--include/events/messages/Video.h70
-rw-r--r--src/ChatPage.cc160
-rw-r--r--src/ImageItem.cc10
-rw-r--r--src/MatrixClient.cc4
-rw-r--r--src/RoomInfoListItem.cc36
-rw-r--r--src/RoomList.cc86
-rw-r--r--src/RoomState.cc32
-rw-r--r--src/Sync.cc28
-rw-r--r--src/TimelineItem.cc30
-rw-r--r--src/TimelineView.cc122
-rw-r--r--src/TimelineViewManager.cc23
-rw-r--r--src/events/Event.cc21
-rw-r--r--src/events/MessageEventContent.cc63
-rw-r--r--src/events/messages/Audio.cc39
-rw-r--r--src/events/messages/Emote.cc26
-rw-r--r--src/events/messages/File.cc51
-rw-r--r--src/events/messages/Image.cc51
-rw-r--r--src/events/messages/Location.cc46
-rw-r--r--src/events/messages/Notice.cc26
-rw-r--r--src/events/messages/Text.cc26
-rw-r--r--src/events/messages/Video.cc52
-rw-r--r--tests/event_collection.cc115
-rw-r--r--tests/events.cc1
-rw-r--r--tests/message_events.cc311
45 files changed, 1902 insertions, 283 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 73054235..a1342e8f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -89,9 +89,9 @@ set(SRC_FILES
     src/MainWindow.cc
     src/MatrixClient.cc
     src/Profile.cc
-    src/RoomInfo.cc
     src/RoomInfoListItem.cc
     src/RoomList.cc
+    src/RoomState.cc
     src/Register.cc
     src/RegisterPage.cc
     src/SlidingStackWidget.cc
@@ -126,14 +126,25 @@ set(MATRIX_EVENTS
     src/events/HistoryVisibilityEventContent.cc
     src/events/JoinRulesEventContent.cc
     src/events/MemberEventContent.cc
+    src/events/MessageEventContent.cc
     src/events/NameEventContent.cc
     src/events/PowerLevelsEventContent.cc
     src/events/TopicEventContent.cc
+
+    src/events/messages/Audio.cc
+    src/events/messages/Emote.cc
+    src/events/messages/File.cc
+    src/events/messages/Image.cc
+    src/events/messages/Location.cc
+    src/events/messages/Notice.cc
+    src/events/messages/Text.cc
+    src/events/messages/Video.cc
 )
 
 include_directories(include)
 include_directories(include/ui)
 include_directories(include/events)
+include_directories(include/events/messages)
 
 qt5_wrap_ui (UI_HEADERS
     forms/ChatPage.ui
@@ -191,7 +202,15 @@ if (BUILD_TESTS)
     add_executable(events_test tests/events.cc)
     target_link_libraries(events_test matrix_events ${GTEST_BOTH_LIBRARIES})
 
+    add_executable(event_collection_test tests/event_collection.cc)
+    target_link_libraries(event_collection_test matrix_events ${GTEST_BOTH_LIBRARIES})
+
+    add_executable(message_events tests/message_events.cc)
+    target_link_libraries(message_events matrix_events ${GTEST_BOTH_LIBRARIES})
+
     add_test(MatrixEvents events_test)
+    add_test(MatrixEventCollection event_collection_test)
+    add_test(MatrixMessageEvents message_events)
 else()
     add_executable (nheko ${OS_BUNDLE} ${SRC_FILES} ${UI_HEADERS} ${MOC_HEADERS} ${QRC})
     target_link_libraries (nheko matrix_events Qt5::Widgets Qt5::Network)
diff --git a/include/ChatPage.h b/include/ChatPage.h
index 074b0753..2107eccf 100644
--- a/include/ChatPage.h
+++ b/include/ChatPage.h
@@ -23,8 +23,8 @@
 #include <QWidget>
 
 #include "MatrixClient.h"
-#include "RoomInfo.h"
 #include "RoomList.h"
+#include "RoomState.h"
 #include "TextInputWidget.h"
 #include "TimelineViewManager.h"
 #include "TopRoomBar.h"
@@ -58,11 +58,13 @@ private slots:
 	void initialSyncCompleted(const SyncResponse &response);
 	void syncCompleted(const SyncResponse &response);
 	void syncFailed(const QString &msg);
-	void changeTopRoomInfo(const RoomInfo &info);
+	void changeTopRoomInfo(const QString &room_id);
 	void startSync();
 	void logout();
 
 private:
+	void updateRoomState(RoomState &room_state, const QJsonArray &events);
+
 	Ui::ChatPage *ui;
 
 	RoomList *room_list_;
@@ -74,11 +76,13 @@ private:
 	QTimer *sync_timer_;
 	int sync_interval_;
 
-	RoomInfo current_room_;
+	QString current_room_;
 	QMap<QString, QPixmap> room_avatars_;
 
 	UserInfoWidget *user_info_widget_;
 
+	QMap<QString, RoomState> state_manager_;
+
 	// Matrix Client API provider.
 	QSharedPointer<MatrixClient> client_;
 };
diff --git a/include/ImageItem.h b/include/ImageItem.h
index 7dc8773f..5d065b25 100644
--- a/include/ImageItem.h
+++ b/include/ImageItem.h
@@ -23,16 +23,18 @@
 #include <QSharedPointer>
 #include <QWidget>
 
+#include "Image.h"
 #include "MatrixClient.h"
 
+namespace events = matrix::events;
+namespace msgs = matrix::events::messages;
+
 class ImageItem : public QWidget
 {
 	Q_OBJECT
 public:
 	ImageItem(QSharedPointer<MatrixClient> client,
-		  const Event &event,
-		  const QString &body,
-		  const QUrl &url,
+		  const events::MessageEvent<msgs::Image> &event,
 		  QWidget *parent = nullptr);
 
 	void setImage(const QPixmap &image);
@@ -65,7 +67,7 @@ private:
 
 	int bottom_height_ = 30;
 
-	Event event_;
+	events::MessageEvent<msgs::Image> event_;
 
 	QSharedPointer<MatrixClient> client_;
 };
diff --git a/include/RoomInfoListItem.h b/include/RoomInfoListItem.h
index 5c403dc3..f45c9324 100644
--- a/include/RoomInfoListItem.h
+++ b/include/RoomInfoListItem.h
@@ -26,26 +26,27 @@
 #include "Avatar.h"
 #include "Badge.h"
 #include "RippleOverlay.h"
-#include "RoomInfo.h"
+#include "RoomState.h"
 
 class RoomInfoListItem : public QWidget
 {
 	Q_OBJECT
 
 public:
-	RoomInfoListItem(RoomInfo info, QWidget *parent = 0);
+	RoomInfoListItem(RoomState state, QString room_id, QWidget *parent = 0);
 	~RoomInfoListItem();
 
 	void updateUnreadMessageCount(int count);
 	void clearUnreadMessageCount();
+	void setState(const RoomState &state);
 
-	inline bool isPressed();
-	inline RoomInfo info();
+	inline bool isPressed() const;
+	inline RoomState state() const;
 	inline void setAvatar(const QImage &avatar_image);
-	inline int unreadMessageCount();
+	inline int unreadMessageCount() const;
 
 signals:
-	void clicked(const RoomInfo &info_);
+	void clicked(const QString &room_id);
 
 public slots:
 	void setPressedState(bool state);
@@ -58,7 +59,8 @@ private:
 
 	RippleOverlay *ripple_overlay_;
 
-	RoomInfo info_;
+	RoomState state_;
+	QString room_id_;
 
 	QHBoxLayout *topLayout_;
 
@@ -83,19 +85,19 @@ private:
 	int unread_msg_count_;
 };
 
-inline int RoomInfoListItem::unreadMessageCount()
+inline int RoomInfoListItem::unreadMessageCount() const
 {
 	return unread_msg_count_;
 }
 
-inline bool RoomInfoListItem::isPressed()
+inline bool RoomInfoListItem::isPressed() const
 {
 	return is_pressed_;
 }
 
-inline RoomInfo RoomInfoListItem::info()
+inline RoomState RoomInfoListItem::state() const
 {
-	return info_;
+	return state_;
 }
 
 inline void RoomInfoListItem::setAvatar(const QImage &avatar_image)
diff --git a/include/RoomList.h b/include/RoomList.h
index e22f0954..8bb962e0 100644
--- a/include/RoomList.h
+++ b/include/RoomList.h
@@ -24,8 +24,8 @@
 #include <QWidget>
 
 #include "MatrixClient.h"
-#include "RoomInfo.h"
 #include "RoomInfoListItem.h"
+#include "RoomState.h"
 #include "Sync.h"
 
 namespace Ui
@@ -41,18 +41,18 @@ public:
 	RoomList(QSharedPointer<MatrixClient> client, QWidget *parent = 0);
 	~RoomList();
 
-	void setInitialRooms(const Rooms &rooms);
-	void clear();
+	void setInitialRooms(const QMap<QString, RoomState> &states);
+	void sync(const QMap<QString, RoomState> &states);
 
-	RoomInfo extractRoomInfo(const State &room_state);
+	void clear();
 
 signals:
-	void roomChanged(const RoomInfo &info);
+	void roomChanged(const QString &room_id);
 	void totalUnreadMessageCountUpdated(int count);
 
 public slots:
 	void updateRoomAvatar(const QString &roomid, const QPixmap &img);
-	void highlightSelectedRoom(const RoomInfo &info);
+	void highlightSelectedRoom(const QString &room_id);
 	void updateUnreadMessageCount(const QString &roomid, int count);
 
 private:
diff --git a/include/RoomState.h b/include/RoomState.h
new file mode 100644
index 00000000..a6cce540
--- /dev/null
+++ b/include/RoomState.h
@@ -0,0 +1,63 @@
+/*
+ * 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 ROOM_STATE_H
+#define ROOM_STATE_H
+
+#include <QPixmap>
+
+#include "AliasesEventContent.h"
+#include "AvatarEventContent.h"
+#include "CanonicalAliasEventContent.h"
+#include "CreateEventContent.h"
+#include "HistoryVisibilityEventContent.h"
+#include "JoinRulesEventContent.h"
+#include "NameEventContent.h"
+#include "PowerLevelsEventContent.h"
+#include "TopicEventContent.h"
+
+#include "Event.h"
+#include "RoomEvent.h"
+#include "StateEvent.h"
+
+namespace events = matrix::events;
+
+class RoomState
+{
+public:
+	QString resolveName() const;
+	inline QString resolveTopic() const;
+
+	QPixmap avatar_img_;
+
+	events::StateEvent<events::AliasesEventContent> aliases;
+	events::StateEvent<events::AvatarEventContent> avatar;
+	events::StateEvent<events::CanonicalAliasEventContent> canonical_alias;
+	events::StateEvent<events::CreateEventContent> create;
+	events::StateEvent<events::HistoryVisibilityEventContent> history_visibility;
+	events::StateEvent<events::JoinRulesEventContent> join_rules;
+	events::StateEvent<events::NameEventContent> name;
+	events::StateEvent<events::PowerLevelsEventContent> power_levels;
+	events::StateEvent<events::TopicEventContent> topic;
+};
+
+inline QString RoomState::resolveTopic() const
+{
+	return topic.content().topic().simplified();
+}
+
+#endif  // ROOM_STATE_H
diff --git a/include/Sync.h b/include/Sync.h
index 6a227bcf..bfa03c1b 100644
--- a/include/Sync.h
+++ b/include/Sync.h
@@ -18,6 +18,7 @@
 #ifndef SYNC_H
 #define SYNC_H
 
+#include <QJsonArray>
 #include <QJsonDocument>
 #include <QMap>
 #include <QString>
@@ -90,13 +91,13 @@ class State : public Deserializable
 {
 public:
 	void deserialize(const QJsonValue &data) override;
-	inline QList<Event> events() const;
+	inline QJsonArray events() const;
 
 private:
-	QList<Event> events_;
+	QJsonArray events_;
 };
 
-inline QList<Event> State::events() const
+inline QJsonArray State::events() const
 {
 	return events_;
 }
@@ -104,19 +105,19 @@ inline QList<Event> State::events() const
 class Timeline : public Deserializable
 {
 public:
-	inline QList<Event> events() const;
+	inline QJsonArray events() const;
 	inline QString previousBatch() const;
 	inline bool limited() const;
 
 	void deserialize(const QJsonValue &data) override;
 
 private:
-	QList<Event> events_;
+	QJsonArray events_;
 	QString prev_batch_;
 	bool limited_;
 };
 
-inline QList<Event> Timeline::events() const
+inline QJsonArray Timeline::events() const
 {
 	return events_;
 }
diff --git a/include/TimelineItem.h b/include/TimelineItem.h
index e9f18f20..f23ad2c9 100644
--- a/include/TimelineItem.h
+++ b/include/TimelineItem.h
@@ -25,20 +25,27 @@
 #include "ImageItem.h"
 #include "Sync.h"
 
+#include "Image.h"
+#include "MessageEvent.h"
+#include "Notice.h"
+#include "Text.h"
+
+namespace events = matrix::events;
+namespace msgs = matrix::events::messages;
+
 class TimelineItem : public QWidget
 {
 	Q_OBJECT
 public:
-	// For remote messages.
-	TimelineItem(const Event &event, bool with_sender, const QString &color, QWidget *parent = 0);
+	TimelineItem(const events::MessageEvent<msgs::Notice> &e, bool with_sender, const QString &color, QWidget *parent = 0);
+	TimelineItem(const events::MessageEvent<msgs::Text> &e, bool with_sender, const QString &color, QWidget *parent = 0);
 
 	// For local messages.
 	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(ImageItem *img, const events::MessageEvent<msgs::Image> &e, const QString &color, QWidget *parent);
+	TimelineItem(ImageItem *img, const events::MessageEvent<msgs::Image> &e, QWidget *parent);
 
 	~TimelineItem();
 
diff --git a/include/TimelineView.h b/include/TimelineView.h
index 4400c361..1808d735 100644
--- a/include/TimelineView.h
+++ b/include/TimelineView.h
@@ -27,6 +27,13 @@
 #include "Sync.h"
 #include "TimelineItem.h"
 
+#include "Image.h"
+#include "Notice.h"
+#include "Text.h"
+
+namespace msgs = matrix::events::messages;
+namespace events = matrix::events;
+
 // Contains info about a message shown in the history view
 // but not yet confirmed by the homeserver through sync.
 struct PendingMessage {
@@ -50,13 +57,14 @@ class TimelineView : public QWidget
 
 public:
 	TimelineView(QSharedPointer<MatrixClient> client, QWidget *parent = 0);
-	TimelineView(const QList<Event> &events, QSharedPointer<MatrixClient> client, QWidget *parent = 0);
+	TimelineView(const QJsonArray &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 addHistoryItem(const events::MessageEvent<msgs::Image> &e, const QString &color, bool with_sender);
+	void addHistoryItem(const events::MessageEvent<msgs::Notice> &e, const QString &color, bool with_sender);
+	void addHistoryItem(const events::MessageEvent<msgs::Text> &e, const QString &color, bool with_sender);
+
+	int addEvents(const QJsonArray &events);
 	void addUserTextMessage(const QString &msg, int txn_id);
 	void updatePendingMessage(int txn_id, QString event_id);
 	void clear();
@@ -66,8 +74,8 @@ public slots:
 
 private:
 	void init();
-	void removePendingMessage(const Event &event);
-	bool isPendingMessage(const Event &event, const QString &userid);
+	void removePendingMessage(const events::MessageEvent<msgs::Text> &e);
+	bool isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &userid);
 
 	QVBoxLayout *top_layout_;
 	QVBoxLayout *scroll_layout_;
diff --git a/include/TimelineViewManager.h b/include/TimelineViewManager.h
index bb4351c4..3c539305 100644
--- a/include/TimelineViewManager.h
+++ b/include/TimelineViewManager.h
@@ -24,7 +24,6 @@
 #include <QWidget>
 
 #include "MatrixClient.h"
-#include "RoomInfo.h"
 #include "Sync.h"
 #include "TimelineView.h"
 
@@ -48,14 +47,14 @@ signals:
 	void unreadMessages(QString roomid, int count);
 
 public slots:
-	void setHistoryView(const RoomInfo &info);
+	void setHistoryView(const QString &room_id);
 	void sendTextMessage(const QString &msg);
 
 private slots:
 	void messageSent(const QString &eventid, const QString &roomid, int txnid);
 
 private:
-	RoomInfo active_room_;
+	QString active_room_;
 	QMap<QString, TimelineView *> views_;
 	QSharedPointer<MatrixClient> client_;
 };
diff --git a/include/events/Event.h b/include/events/Event.h
index 2621eadb..a7e4fb2d 100644
--- a/include/events/Event.h
+++ b/include/events/Event.h
@@ -41,6 +41,8 @@ enum EventType {
 	RoomJoinRules,
 	/// m.room.member
 	RoomMember,
+	/// m.room.message
+	RoomMessage,
 	/// m.room.name
 	RoomName,
 	/// m.room.power_levels
@@ -53,6 +55,9 @@ enum EventType {
 
 EventType extractEventType(const QJsonObject &data);
 
+bool isMessageEvent(EventType type);
+bool isStateEvent(EventType type);
+
 template <class Content>
 class Event : public Deserializable
 {
diff --git a/include/events/MessageEvent.h b/include/events/MessageEvent.h
new file mode 100644
index 00000000..617514b0
--- /dev/null
+++ b/include/events/MessageEvent.h
@@ -0,0 +1,67 @@
+/*
+ * 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 MATRIX_MESSAGE_EVENT_H
+#define MATRIX_MESSAGE_EVENT_H
+
+#include "MessageEventContent.h"
+#include "RoomEvent.h"
+
+namespace matrix
+{
+namespace events
+{
+template <class MsgContent>
+class MessageEvent : public RoomEvent<MessageEventContent>
+{
+public:
+	inline MsgContent msgContent() const;
+
+	void deserialize(const QJsonValue &data) override;
+
+private:
+	MsgContent msg_content_;
+};
+
+template <class MsgContent>
+inline MsgContent MessageEvent<MsgContent>::msgContent() const
+{
+	return msg_content_;
+}
+
+template <class MsgContent>
+void MessageEvent<MsgContent>::deserialize(const QJsonValue &data)
+{
+	RoomEvent<MessageEventContent>::deserialize(data);
+
+	msg_content_.deserialize(data.toObject().value("content").toObject());
+}
+
+namespace messages
+{
+struct ThumbnailInfo {
+	int h;
+	int w;
+	int size;
+
+	QString mimetype;
+};
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MATRIX_MESSAGE_EVENT_H
diff --git a/include/events/MessageEventContent.h b/include/events/MessageEventContent.h
new file mode 100644
index 00000000..adc0f3ff
--- /dev/null
+++ b/include/events/MessageEventContent.h
@@ -0,0 +1,78 @@
+/*
+ * 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 MESSAGE_EVENT_CONTENT_H
+#define MESSAGE_EVENT_CONTENT_H
+
+#include <QJsonValue>
+
+#include "Deserializable.h"
+
+namespace matrix
+{
+namespace events
+{
+enum MessageEventType {
+	// m.audio
+	Audio,
+
+	// m.emote
+	Emote,
+
+	// m.file
+	File,
+
+	// m.image
+	Image,
+
+	// m.location
+	Location,
+
+	// m.notice
+	Notice,
+
+	// m.text
+	Text,
+
+	// m.video
+	Video,
+
+	// Unrecognized message type
+	Unknown,
+};
+
+MessageEventType extractMessageEventType(const QJsonObject &data);
+
+class MessageEventContent : public Deserializable
+{
+public:
+	void deserialize(const QJsonValue &data) override;
+
+	inline QString body() const;
+
+private:
+	QString body_;
+};
+
+inline QString MessageEventContent::body() const
+{
+	return body_;
+}
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_CONTENT_H
diff --git a/include/events/RoomEvent.h b/include/events/RoomEvent.h
index d8fa6e0e..9c2e9945 100644
--- a/include/events/RoomEvent.h
+++ b/include/events/RoomEvent.h
@@ -83,8 +83,9 @@ void RoomEvent<Content>::deserialize(const QJsonValue &data)
 	if (!object.contains("origin_server_ts"))
 		throw DeserializationException("origin_server_ts key is missing");
 
-	if (!object.contains("room_id"))
-		throw DeserializationException("room_id key is missing");
+	// FIXME: Synapse doesn't include room id?!
+	/* if (!object.contains("room_id")) */
+	/* 	throw DeserializationException("room_id key is missing"); */
 
 	if (!object.contains("sender"))
 		throw DeserializationException("sender key is missing");
diff --git a/src/RoomInfo.cc b/include/events/messages/Audio.h
index f8a7c56a..c3b5a4ef 100644
--- a/src/RoomInfo.cc
+++ b/include/events/messages/Audio.h
@@ -15,57 +15,51 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#include "RoomInfo.h"
+#ifndef MESSAGE_EVENT_AUDIO_H
+#define MESSAGE_EVENT_AUDIO_H
 
-RoomInfo::RoomInfo()
-    : name_("")
-    , topic_("")
-{
-}
+#include <QJsonObject>
 
-RoomInfo::RoomInfo(QString name, QString topic, QUrl avatar_url)
-    : name_(name)
-    , topic_(topic)
-    , avatar_url_(avatar_url)
-{
-}
+#include "Deserializable.h"
 
-QString RoomInfo::id() const
+namespace matrix
 {
-	return id_;
-}
-
-QString RoomInfo::name() const
+namespace events
 {
-	return name_;
-}
-
-QString RoomInfo::topic() const
+namespace messages
 {
-	return topic_;
-}
+struct AudioInfo {
+	uint64_t duration;
+	int size;
 
-QUrl RoomInfo::avatarUrl() const
-{
-	return avatar_url_;
-}
+	QString mimetype;
+};
 
-void RoomInfo::setAvatarUrl(const QUrl &url)
+class Audio : public Deserializable
 {
-	avatar_url_ = url;
-}
+public:
+	inline QString url() const;
+	inline AudioInfo info() const;
 
-void RoomInfo::setId(const QString &id)
-{
-	id_ = id;
-}
+	void deserialize(const QJsonObject &object) override;
+
+private:
+	QString url_;
+	AudioInfo info_;
+};
 
-void RoomInfo::setName(const QString &name)
+inline QString Audio::url() const
 {
-	name_ = name;
+	return url_;
 }
 
-void RoomInfo::setTopic(const QString &topic)
+inline AudioInfo Audio::info() const
 {
-	topic_ = topic;
+	return info_;
 }
+
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_AUDIO_H
diff --git a/include/RoomInfo.h b/include/events/messages/Emote.h
index 9976ba8a..63b2b96b 100644
--- a/include/RoomInfo.h
+++ b/include/events/messages/Emote.h
@@ -1,3 +1,4 @@
+
 /*
  * nheko Copyright (C) 2017  Konstantinos Sideris <siderisk@auth.gr>
  *
@@ -15,35 +16,26 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef ROOM_INFO_H
-#define ROOM_INFO_H
+#ifndef MESSAGE_EVENT_EMOTE_H
+#define MESSAGE_EVENT_EMOTE_H
+
+#include <QJsonObject>
 
-#include <QList>
-#include <QString>
-#include <QUrl>
+#include "Deserializable.h"
 
-class RoomInfo
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+class Emote : public Deserializable
 {
 public:
-	RoomInfo();
-	RoomInfo(QString name, QString topic = "", QUrl avatar_url = QUrl(""));
-
-	QString id() const;
-	QString name() const;
-	QString topic() const;
-	QUrl avatarUrl() const;
-
-	void setAvatarUrl(const QUrl &url);
-	void setId(const QString &id);
-	void setName(const QString &name);
-	void setTopic(const QString &name);
-
-private:
-	QString id_;
-	QString name_;
-	QString topic_;
-	QUrl avatar_url_;
-	QList<QString> aliases_;
+	void deserialize(const QJsonObject &obj) override;
 };
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
 
-#endif  // ROOM_INFO_H
+#endif  // MESSAGE_EVENT_EMOTE_H
diff --git a/include/events/messages/File.h b/include/events/messages/File.h
new file mode 100644
index 00000000..8fe61615
--- /dev/null
+++ b/include/events/messages/File.h
@@ -0,0 +1,76 @@
+/*
+ * 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 MESSAGE_EVENT_FILE_H
+#define MESSAGE_EVENT_FILE_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+#include "MessageEvent.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+struct FileInfo {
+	int size;
+
+	QString mimetype;
+	QString thumbnail_url;
+	ThumbnailInfo thumbnail_info;
+};
+
+class File : public Deserializable
+{
+public:
+	inline QString url() const;
+	inline QString filename() const;
+
+	inline FileInfo info() const;
+
+	void deserialize(const QJsonObject &object) override;
+
+private:
+	QString url_;
+	QString filename_;
+
+	FileInfo info_;
+};
+
+inline QString File::filename() const
+{
+	return filename_;
+}
+
+inline QString File::url() const
+{
+	return url_;
+}
+
+inline FileInfo File::info() const
+{
+	return info_;
+}
+
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_FILE_H
diff --git a/include/events/messages/Image.h b/include/events/messages/Image.h
new file mode 100644
index 00000000..5a329e4d
--- /dev/null
+++ b/include/events/messages/Image.h
@@ -0,0 +1,69 @@
+/*
+ * 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 MESSAGE_EVENT_IMAGE_H
+#define MESSAGE_EVENT_IMAGE_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+#include "MessageEvent.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+struct ImageInfo {
+	int h;
+	int w;
+	int size;
+
+	QString mimetype;
+	QString thumbnail_url;
+	ThumbnailInfo thumbnail_info;
+};
+
+class Image : public Deserializable
+{
+public:
+	inline QString url() const;
+	inline ImageInfo info() const;
+
+	void deserialize(const QJsonObject &object) override;
+
+private:
+	QString url_;
+	ImageInfo info_;
+};
+
+inline QString Image::url() const
+{
+	return url_;
+}
+
+inline ImageInfo Image::info() const
+{
+	return info_;
+}
+
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_IMAGE_H
diff --git a/include/events/messages/Location.h b/include/events/messages/Location.h
new file mode 100644
index 00000000..7c64cede
--- /dev/null
+++ b/include/events/messages/Location.h
@@ -0,0 +1,65 @@
+/*
+ * 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 MESSAGE_EVENT_LOCATION_H
+#define MESSAGE_EVENT_LOCATION_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+#include "MessageEvent.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+struct LocationInfo {
+	QString thumbnail_url;
+	ThumbnailInfo thumbnail_info;
+};
+
+class Location : public Deserializable
+{
+public:
+	inline QString geoUri() const;
+	inline LocationInfo info() const;
+
+	void deserialize(const QJsonObject &object) override;
+
+private:
+	QString geo_uri_;
+
+	LocationInfo info_;
+};
+
+inline QString Location::geoUri() const
+{
+	return geo_uri_;
+}
+
+inline LocationInfo Location::info() const
+{
+	return info_;
+}
+
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_LOCATION_H
diff --git a/include/events/messages/Notice.h b/include/events/messages/Notice.h
new file mode 100644
index 00000000..db94b273
--- /dev/null
+++ b/include/events/messages/Notice.h
@@ -0,0 +1,40 @@
+/*
+ * 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 MESSAGE_EVENT_NOTICE_H
+#define MESSAGE_EVENT_NOTICE_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+class Notice : public Deserializable
+{
+public:
+	void deserialize(const QJsonObject &obj) override;
+};
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_NOTICE_H
diff --git a/include/events/messages/Text.h b/include/events/messages/Text.h
new file mode 100644
index 00000000..f116e78d
--- /dev/null
+++ b/include/events/messages/Text.h
@@ -0,0 +1,40 @@
+/*
+ * 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 MESSAGE_EVENT_TEXT_H
+#define MESSAGE_EVENT_TEXT_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+class Text : public Deserializable
+{
+public:
+	void deserialize(const QJsonObject &obj) override;
+};
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_TEXT_H
diff --git a/include/events/messages/Video.h b/include/events/messages/Video.h
new file mode 100644
index 00000000..bd307cf7
--- /dev/null
+++ b/include/events/messages/Video.h
@@ -0,0 +1,70 @@
+/*
+ * 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 MESSAGE_EVENT_VIDEO_H
+#define MESSAGE_EVENT_VIDEO_H
+
+#include <QJsonObject>
+
+#include "Deserializable.h"
+#include "MessageEvent.h"
+
+namespace matrix
+{
+namespace events
+{
+namespace messages
+{
+struct VideoInfo {
+	int h;
+	int w;
+	int size;
+	int duration;
+
+	QString mimetype;
+	QString thumbnail_url;
+	ThumbnailInfo thumbnail_info;
+};
+
+class Video : public Deserializable
+{
+public:
+	inline QString url() const;
+	inline VideoInfo info() const;
+
+	void deserialize(const QJsonObject &object) override;
+
+private:
+	QString url_;
+	VideoInfo info_;
+};
+
+inline QString Video::url() const
+{
+	return url_;
+}
+
+inline VideoInfo Video::info() const
+{
+	return info_;
+}
+
+}  // namespace messages
+}  // namespace events
+}  // namespace matrix
+
+#endif  // MESSAGE_EVENT_VIDEO_H
diff --git a/src/ChatPage.cc b/src/ChatPage.cc
index 0ddf0f8b..fbaf9ddd 100644
--- a/src/ChatPage.cc
+++ b/src/ChatPage.cc
@@ -25,6 +25,20 @@
 #include "Sync.h"
 #include "UserInfoWidget.h"
 
+#include "AliasesEventContent.h"
+#include "AvatarEventContent.h"
+#include "CanonicalAliasEventContent.h"
+#include "CreateEventContent.h"
+#include "HistoryVisibilityEventContent.h"
+#include "JoinRulesEventContent.h"
+#include "NameEventContent.h"
+#include "PowerLevelsEventContent.h"
+#include "TopicEventContent.h"
+
+#include "StateEvent.h"
+
+namespace events = matrix::events;
+
 ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
     : QWidget(parent)
     , ui(new Ui::ChatPage)
@@ -55,16 +69,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent)
 	connect(user_info_widget_, SIGNAL(logout()), client_.data(), SLOT(logout()));
 	connect(client_.data(), SIGNAL(loggedOut()), this, SLOT(logout()));
 
-	connect(room_list_,
-		SIGNAL(roomChanged(const RoomInfo &)),
-		this,
-		SLOT(changeTopRoomInfo(const RoomInfo &)));
-	connect(room_list_,
-		SIGNAL(roomChanged(const RoomInfo &)),
-		view_manager_,
-		SLOT(setHistoryView(const RoomInfo &)));
+	connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
+	connect(room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView);
 
-	// TODO: Better pass the whole RoomInfo struct instead of the roomid.
 	connect(view_manager_,
 		SIGNAL(unreadMessages(const QString &, int)),
 		room_list_,
@@ -161,7 +168,24 @@ void ChatPage::syncCompleted(const SyncResponse &response)
 {
 	client_->setNextBatchToken(response.nextBatch());
 
-	/* room_list_->sync(response.rooms()); */
+	auto joined = response.rooms().join();
+
+	for (auto it = joined.constBegin(); it != joined.constEnd(); it++) {
+		RoomState room_state;
+
+		if (state_manager_.contains(it.key()))
+			room_state = state_manager_[it.key()];
+
+		updateRoomState(room_state, it.value().state().events());
+		updateRoomState(room_state, it.value().timeline().events());
+
+		state_manager_.insert(it.key(), room_state);
+
+		if (it.key() == current_room_)
+			changeTopRoomInfo(it.key());
+	}
+
+	room_list_->sync(state_manager_);
 	view_manager_->sync(response.rooms());
 
 	sync_timer_->start(sync_interval_);
@@ -172,8 +196,19 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response)
 	if (!response.nextBatch().isEmpty())
 		client_->setNextBatchToken(response.nextBatch());
 
+	auto joined = response.rooms().join();
+
+	for (auto it = joined.constBegin(); it != joined.constEnd(); it++) {
+		RoomState room_state;
+
+		updateRoomState(room_state, it.value().state().events());
+		updateRoomState(room_state, it.value().timeline().events());
+
+		state_manager_.insert(it.key(), room_state);
+	}
+
 	view_manager_->initialize(response.rooms());
-	room_list_->setInitialRooms(response.rooms());
+	room_list_->setInitialRooms(state_manager_);
 
 	sync_timer_->start(sync_interval_);
 }
@@ -182,7 +217,7 @@ void ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img)
 {
 	room_avatars_.insert(roomid, img);
 
-	if (current_room_.id() != roomid)
+	if (current_room_ != roomid)
 		return;
 
 	top_bar_->updateRoomAvatar(img.toImage());
@@ -199,17 +234,22 @@ void ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &displ
 	client_->fetchOwnAvatar(avatar_url);
 }
 
-void ChatPage::changeTopRoomInfo(const RoomInfo &info)
+void ChatPage::changeTopRoomInfo(const QString &room_id)
 {
-	top_bar_->updateRoomName(info.name());
-	top_bar_->updateRoomTopic(info.topic());
+	if (!state_manager_.contains(room_id))
+		return;
 
-	if (room_avatars_.contains(info.id()))
-		top_bar_->updateRoomAvatar(room_avatars_.value(info.id()).toImage());
+	auto state = state_manager_[room_id];
+
+	top_bar_->updateRoomName(state.resolveName());
+	top_bar_->updateRoomTopic(state.resolveTopic());
+
+	if (room_avatars_.contains(room_id))
+		top_bar_->updateRoomAvatar(room_avatars_.value(room_id).toImage());
 	else
-		top_bar_->updateRoomAvatarFromName(info.name());
+		top_bar_->updateRoomAvatarFromName(state.resolveName());
 
-	current_room_ = info;
+	current_room_ = room_id;
 }
 
 void ChatPage::showUnreadMessageNotification(int count)
@@ -221,6 +261,88 @@ void ChatPage::showUnreadMessageNotification(int count)
 		emit changeWindowTitle(QString("nheko (%1)").arg(count));
 }
 
+void ChatPage::updateRoomState(RoomState &room_state, const QJsonArray &events)
+{
+	events::EventType ty;
+
+	for (const auto &event : events) {
+		try {
+			ty = events::extractEventType(event.toObject());
+		} catch (const DeserializationException &e) {
+			qWarning() << e.what() << event;
+			continue;
+		}
+
+		if (!events::isStateEvent(ty))
+			continue;
+
+		try {
+			switch (ty) {
+			case events::EventType::RoomAliases: {
+				events::StateEvent<events::AliasesEventContent> aliases;
+				aliases.deserialize(event);
+				room_state.aliases = aliases;
+				break;
+			}
+			case events::EventType::RoomAvatar: {
+				events::StateEvent<events::AvatarEventContent> avatar;
+				avatar.deserialize(event);
+				room_state.avatar = avatar;
+				break;
+			}
+			case events::EventType::RoomCanonicalAlias: {
+				events::StateEvent<events::CanonicalAliasEventContent> canonical_alias;
+				canonical_alias.deserialize(event);
+				room_state.canonical_alias = canonical_alias;
+				break;
+			}
+			case events::EventType::RoomCreate: {
+				events::StateEvent<events::CreateEventContent> create;
+				create.deserialize(event);
+				room_state.create = create;
+				break;
+			}
+			case events::EventType::RoomHistoryVisibility: {
+				events::StateEvent<events::HistoryVisibilityEventContent> history_visibility;
+				history_visibility.deserialize(event);
+				room_state.history_visibility = history_visibility;
+				break;
+			}
+			case events::EventType::RoomJoinRules: {
+				events::StateEvent<events::JoinRulesEventContent> join_rules;
+				join_rules.deserialize(event);
+				room_state.join_rules = join_rules;
+				break;
+			}
+			case events::EventType::RoomName: {
+				events::StateEvent<events::NameEventContent> name;
+				name.deserialize(event);
+				room_state.name = name;
+				break;
+			}
+			case events::EventType::RoomPowerLevels: {
+				events::StateEvent<events::PowerLevelsEventContent> power_levels;
+				power_levels.deserialize(event);
+				room_state.power_levels = power_levels;
+				break;
+			}
+			case events::EventType::RoomTopic: {
+				events::StateEvent<events::TopicEventContent> topic;
+				topic.deserialize(event);
+				room_state.topic = topic;
+				break;
+			}
+			default: {
+				continue;
+			}
+			}
+		} catch (const DeserializationException &e) {
+			qWarning() << e.what() << event;
+			continue;
+		}
+	}
+}
+
 ChatPage::~ChatPage()
 {
 	sync_timer_->stop();
diff --git a/src/ImageItem.cc b/src/ImageItem.cc
index d03e41b5..e0e2f977 100644
--- a/src/ImageItem.cc
+++ b/src/ImageItem.cc
@@ -25,10 +25,11 @@
 #include "ImageItem.h"
 #include "ImageOverlayDialog.h"
 
-ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const Event &event, const QString &body, const QUrl &url, QWidget *parent)
+namespace events = matrix::events;
+namespace msgs = matrix::events::messages;
+
+ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const events::MessageEvent<msgs::Image> &event, QWidget *parent)
     : QWidget(parent)
-    , url_{url}
-    , text_{body}
     , event_{event}
     , client_{client}
 {
@@ -37,6 +38,9 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const Event &event, co
 	setCursor(Qt::PointingHandCursor);
 	setAttribute(Qt::WA_Hover, true);
 
+	url_ = event.msgContent().url();
+	text_ = event.content().body();
+
 	QList<QString> url_parts = url_.toString().split("mxc://");
 
 	if (url_parts.size() != 2) {
diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 6b4a81bb..f9d81f27 100644
--- a/src/MatrixClient.cc
+++ b/src/MatrixClient.cc
@@ -194,10 +194,12 @@ void MatrixClient::onInitialSyncResponse(QNetworkReply *reply)
 
 	try {
 		response.deserialize(json);
-		emit initialSyncCompleted(response);
 	} catch (DeserializationException &e) {
 		qWarning() << "Sync malformed response" << e.what();
+		return;
 	}
+
+	emit initialSyncCompleted(response);
 }
 
 void MatrixClient::onSyncResponse(QNetworkReply *reply)
diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
index 954025c6..6e632a6a 100644
--- a/src/RoomInfoListItem.cc
+++ b/src/RoomInfoListItem.cc
@@ -19,12 +19,13 @@
 #include <QMouseEvent>
 
 #include "Ripple.h"
-#include "RoomInfo.h"
 #include "RoomInfoListItem.h"
+#include "RoomState.h"
 
-RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
+RoomInfoListItem::RoomInfoListItem(RoomState state, QString room_id, QWidget *parent)
     : QWidget(parent)
-    , info_(info)
+    , state_(state)
+    , room_id_(room_id)
     , is_pressed_(false)
     , max_height_(60)
     , unread_msg_count_(0)
@@ -43,6 +44,9 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
 
 	setMaximumSize(parent->width(), max_height_);
 
+	QString room_name = state_.resolveName();
+	QString room_topic = state_.topic.content().topic().simplified();
+
 	topLayout_ = new QHBoxLayout(this);
 	topLayout_->setSpacing(0);
 	topLayout_->setMargin(0);
@@ -60,7 +64,7 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
 	textLayout_->setContentsMargins(0, 5, 0, 5);
 
 	roomAvatar_ = new Avatar(avatarWidget_);
-	roomAvatar_->setLetter(QChar(info_.name()[0]));
+	roomAvatar_->setLetter(QChar(room_name[0]));
 	roomAvatar_->setSize(max_height_ - 20);
 	roomAvatar_->setTextColor("#555459");
 	roomAvatar_->setBackgroundColor("#d6dde3");
@@ -76,12 +80,12 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
 
 	avatarLayout_->addWidget(roomAvatar_);
 
-	roomName_ = new QLabel(info_.name(), textWidget_);
+	roomName_ = new QLabel(room_name, textWidget_);
 	roomName_->setMaximumSize(parent->width() - max_height_, 20);
 	roomName_->setFont(QFont("Open Sans", 11));
 	roomName_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
 
-	roomTopic_ = new QLabel(info_.topic(), textWidget_);
+	roomTopic_ = new QLabel(room_topic, textWidget_);
 	roomTopic_->setMaximumSize(parent->width() - max_height_, 20);
 	roomTopic_->setFont(QFont("Open Sans", 10));
 	roomTopic_->setStyleSheet("color: #171919");
@@ -93,8 +97,8 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent)
 	topLayout_->addWidget(avatarWidget_);
 	topLayout_->addWidget(textWidget_);
 
-	setElidedText(roomName_, info_.name(), parent->width() - max_height_);
-	setElidedText(roomTopic_, info_.topic(), parent->width() - max_height_);
+	setElidedText(roomName_, room_name, parent->width() - max_height_);
+	setElidedText(roomTopic_, room_topic, parent->width() - max_height_);
 
 	QPainterPath path;
 	path.addRoundedRect(rect(), 0, 0);
@@ -131,9 +135,23 @@ void RoomInfoListItem::setPressedState(bool state)
 	}
 }
 
+void RoomInfoListItem::setState(const RoomState &new_state)
+{
+	if (state_.resolveName() != new_state.resolveName())
+		setElidedText(roomName_, new_state.resolveName(), parentWidget()->width() - max_height_);
+
+	if (state_.resolveTopic() != new_state.resolveTopic())
+		setElidedText(roomTopic_, new_state.resolveTopic(), parentWidget()->width() - max_height_);
+
+	if (new_state.avatar.content().url().toString().isEmpty())
+		roomAvatar_->setLetter(QChar(new_state.resolveName()[0]));
+
+	state_ = new_state;
+}
+
 void RoomInfoListItem::mousePressEvent(QMouseEvent *event)
 {
-	emit clicked(info_);
+	emit clicked(room_id_);
 
 	setPressedState(true);
 
diff --git a/src/RoomList.cc b/src/RoomList.cc
index 4fbccee0..a0312113 100644
--- a/src/RoomList.cc
+++ b/src/RoomList.cc
@@ -57,32 +57,6 @@ void RoomList::clear()
 	rooms_.clear();
 }
 
-RoomInfo RoomList::extractRoomInfo(const State &room_state)
-{
-	RoomInfo info;
-
-	auto events = room_state.events();
-
-	for (const auto &event : events) {
-		if (event.type() == "m.room.name") {
-			info.setName(event.content().value("name").toString());
-		} else if (event.type() == "m.room.topic") {
-			info.setTopic(event.content().value("topic").toString());
-		} else if (event.type() == "m.room.avatar") {
-			info.setAvatarUrl(QUrl(event.content().value("url").toString()));
-		} else if (event.type() == "m.room.canonical_alias") {
-			if (info.name().isEmpty())
-				info.setName(event.content().value("alias").toString());
-		}
-	}
-
-	// Sanitize info for print.
-	info.setTopic(info.topic().simplified());
-	info.setName(info.name().simplified());
-
-	return info;
-}
-
 void RoomList::updateUnreadMessageCount(const QString &roomid, int count)
 {
 	if (!rooms_.contains(roomid)) {
@@ -105,27 +79,21 @@ void RoomList::calculateUnreadMessageCount()
 	emit totalUnreadMessageCountUpdated(total_unread_msgs);
 }
 
-void RoomList::setInitialRooms(const Rooms &rooms)
+void RoomList::setInitialRooms(const QMap<QString, RoomState> &states)
 {
 	rooms_.clear();
 
-	for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) {
-		RoomInfo info = RoomList::extractRoomInfo(it.value().state());
-		info.setId(it.key());
-
-		if (info.name().isEmpty())
-			continue;
+	for (auto it = states.constBegin(); it != states.constEnd(); it++) {
+		auto room_id = it.key();
+		auto state = it.value();
 
-		if (!info.avatarUrl().isEmpty())
-			client_->fetchRoomAvatar(info.id(), info.avatarUrl());
+		if (!state.avatar.content().url().toString().isEmpty())
+			client_->fetchRoomAvatar(room_id, state.avatar.content().url());
 
-		RoomInfoListItem *room_item = new RoomInfoListItem(info, ui->scrollArea);
-		connect(room_item,
-			SIGNAL(clicked(const RoomInfo &)),
-			this,
-			SLOT(highlightSelectedRoom(const RoomInfo &)));
+		RoomInfoListItem *room_item = new RoomInfoListItem(state, room_id, ui->scrollArea);
+		connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom);
 
-		rooms_.insert(it.key(), room_item);
+		rooms_.insert(room_id, room_item);
 
 		int pos = ui->scrollVerticalLayout->count() - 1;
 		ui->scrollVerticalLayout->insertWidget(pos, room_item);
@@ -134,29 +102,51 @@ void RoomList::setInitialRooms(const Rooms &rooms)
 	if (rooms_.isEmpty())
 		return;
 
-	// TODO: Move this into its own function.
 	auto first_room = rooms_.first();
 	first_room->setPressedState(true);
-	emit roomChanged(first_room->info());
+
+	emit roomChanged(rooms_.firstKey());
+}
+
+void RoomList::sync(const QMap<QString, RoomState> &states)
+{
+	for (auto it = states.constBegin(); it != states.constEnd(); it++) {
+		auto room_id = it.key();
+		auto state = it.value();
+
+		// TODO: Add the new room to the list.
+		if (!rooms_.contains(room_id))
+			continue;
+
+		auto room = rooms_[room_id];
+
+		auto current_avatar = room->state().avatar.content().url();
+		auto new_avatar = state.avatar.content().url();
+
+		if (current_avatar != new_avatar && !new_avatar.toString().isEmpty())
+			client_->fetchRoomAvatar(room_id, new_avatar);
+
+		room->setState(state);
+	}
 }
 
-void RoomList::highlightSelectedRoom(const RoomInfo &info)
+void RoomList::highlightSelectedRoom(const QString &room_id)
 {
-	emit roomChanged(info);
+	emit roomChanged(room_id);
 
-	if (!rooms_.contains(info.id())) {
+	if (!rooms_.contains(room_id)) {
 		qDebug() << "RoomList: clicked unknown roomid";
 		return;
 	}
 
 	// TODO: Send a read receipt for the last event.
-	auto room = rooms_[info.id()];
+	auto room = rooms_[room_id];
 	room->clearUnreadMessageCount();
 
 	calculateUnreadMessageCount();
 
 	for (auto it = rooms_.constBegin(); it != rooms_.constEnd(); it++) {
-		if (it.key() != info.id())
+		if (it.key() != room_id)
 			it.value()->setPressedState(false);
 	}
 }
diff --git a/src/RoomState.cc b/src/RoomState.cc
new file mode 100644
index 00000000..98f418e3
--- /dev/null
+++ b/src/RoomState.cc
@@ -0,0 +1,32 @@
+/*
+ * 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 "RoomState.h"
+
+QString RoomState::resolveName() const
+{
+	if (!name.content().name().isEmpty())
+		return name.content().name().simplified();
+
+	if (!canonical_alias.content().alias().isEmpty())
+		return canonical_alias.content().alias().simplified();
+
+	if (aliases.content().aliases().size() != 0)
+		return aliases.content().aliases()[0].simplified();
+
+	return "Unknown Room Name";
+}
diff --git a/src/Sync.cc b/src/Sync.cc
index 50b49fc6..0d04e878 100644
--- a/src/Sync.cc
+++ b/src/Sync.cc
@@ -157,19 +157,7 @@ void State::deserialize(const QJsonValue &data)
 	if (!data.isArray())
 		throw DeserializationException("State is not a JSON array");
 
-	QJsonArray event_array = data.toArray();
-
-	for (int i = 0; i < event_array.count(); i++) {
-		Event event;
-
-		try {
-			event.deserialize(event_array.at(i));
-			events_.push_back(event);
-		} catch (DeserializationException &e) {
-			qWarning() << e.what();
-			qWarning() << "Skipping malformed state event";
-		}
-	}
+	events_ = data.toArray();
 }
 
 void Timeline::deserialize(const QJsonValue &data)
@@ -194,17 +182,5 @@ void Timeline::deserialize(const QJsonValue &data)
 	if (!object.value("events").isArray())
 		throw DeserializationException("timeline/events is not a JSON array");
 
-	auto timeline_events = object.value("events").toArray();
-
-	for (int i = 0; i < timeline_events.count(); i++) {
-		Event event;
-
-		try {
-			event.deserialize(timeline_events.at(i));
-			events_.push_back(event);
-		} catch (DeserializationException &e) {
-			qWarning() << e.what();
-			qWarning() << "Skipping malformed timeline event";
-		}
-	}
+	events_ = object.value("events").toArray();
 }
diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc
index 4d33db70..8d5e503a 100644
--- a/src/TimelineItem.cc
+++ b/src/TimelineItem.cc
@@ -21,6 +21,9 @@
 #include "ImageItem.h"
 #include "TimelineItem.h"
 
+namespace events = matrix::events;
+namespace msgs = matrix::events::messages;
+
 TimelineItem::TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent)
     : QWidget(parent)
 {
@@ -37,7 +40,7 @@ TimelineItem::TimelineItem(const QString &body, QWidget *parent)
 	setupLayout();
 }
 
-TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent)
+TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, const QString &color, QWidget *parent)
     : QWidget(parent)
 {
 	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
@@ -58,7 +61,7 @@ TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString &
 	setLayout(top_layout_);
 }
 
-TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent)
+TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, QWidget *parent)
     : QWidget(parent)
 {
 	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
@@ -73,16 +76,31 @@ TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent
 	setLayout(top_layout_);
 }
 
-TimelineItem::TimelineItem(const Event &event, bool with_sender, const QString &color, QWidget *parent)
+TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event, bool with_sender, const QString &color, QWidget *parent)
     : QWidget(parent)
 {
-	auto body = event.content().value("body").toString().trimmed().toHtmlEscaped();
+	auto body = event.content().body().trimmed().toHtmlEscaped();
 	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
 
 	generateTimestamp(timestamp);
 
-	if (event.content().value("msgtype").toString() == "m.notice")
-		body = "<i style=\"color: #565E5E\">" + body + "</i>";
+	body = "<i style=\"color: #565E5E\">" + body + "</i>";
+
+	if (with_sender)
+		generateBody(event.sender(), color, body);
+	else
+		generateBody(body);
+
+	setupLayout();
+}
+
+TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event, bool with_sender, const QString &color, QWidget *parent)
+    : QWidget(parent)
+{
+	auto body = event.content().body().trimmed().toHtmlEscaped();
+	auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp());
+
+	generateTimestamp(timestamp);
 
 	if (with_sender)
 		generateBody(event.sender(), color, body);
diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 95c7a351..686fd602 100644
--- a/src/TimelineView.cc
+++ b/src/TimelineView.cc
@@ -16,17 +16,25 @@
  */
 
 #include <QDebug>
+#include <QJsonArray>
 #include <QScrollBar>
 #include <QSettings>
 #include <QtWidgets/QLabel>
 #include <QtWidgets/QSpacerItem>
 
+#include "Event.h"
+#include "MessageEvent.h"
+#include "MessageEventContent.h"
+
 #include "ImageItem.h"
 #include "TimelineItem.h"
 #include "TimelineView.h"
 #include "TimelineViewManager.h"
 
-TimelineView::TimelineView(const QList<Event> &events, QSharedPointer<MatrixClient> client, QWidget *parent)
+namespace events = matrix::events;
+namespace msgs = matrix::events::messages;
+
+TimelineView::TimelineView(const QJsonArray &events, QSharedPointer<MatrixClient> client, QWidget *parent)
     : QWidget(parent)
     , client_{client}
 {
@@ -53,52 +61,79 @@ void TimelineView::sliderRangeChanged(int min, int max)
 	scroll_area_->verticalScrollBar()->setValue(max);
 }
 
-int TimelineView::addEvents(const QList<Event> &events)
+int TimelineView::addEvents(const QJsonArray &events)
 {
 	QSettings settings;
 	auto local_user = settings.value("auth/user_id").toString();
 
 	int message_count = 0;
+	events::EventType ty;
 
 	for (const auto &event : events) {
-		if (event.type() == "m.room.message") {
-			auto msg_type = event.content().value("msgtype").toString();
+		ty = events::extractEventType(event.toObject());
 
-			if (isPendingMessage(event, local_user)) {
-				removePendingMessage(event);
-				continue;
-			}
+		if (ty == events::RoomMessage) {
+			events::MessageEventType msg_type = events::extractMessageEventType(event.toObject());
 
-			if (msg_type == "m.text" || msg_type == "m.notice") {
-				auto with_sender = last_sender_ != event.sender();
-				auto color = TimelineViewManager::getUserColor(event.sender());
+			if (msg_type == events::MessageEventType::Text) {
+				events::MessageEvent<msgs::Text> text;
 
-				addHistoryItem(event, color, with_sender);
-				last_sender_ = event.sender();
+				try {
+					text.deserialize(event.toObject());
+				} catch (const DeserializationException &e) {
+					qWarning() << e.what() << event;
+					continue;
+				}
 
-				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();
+				if (isPendingMessage(text, local_user)) {
+					removePendingMessage(text);
 					continue;
 				}
 
-				if (!event.content().contains("body")) {
-					qWarning() << "Missing body from m.image event" << event.content();
+				auto with_sender = last_sender_ != text.sender();
+				auto color = TimelineViewManager::getUserColor(text.sender());
+
+				addHistoryItem(text, color, with_sender);
+				last_sender_ = text.sender();
+
+				message_count += 1;
+			} else if (msg_type == events::MessageEventType::Notice) {
+				events::MessageEvent<msgs::Notice> notice;
+
+				try {
+					notice.deserialize(event.toObject());
+				} catch (const DeserializationException &e) {
+					qWarning() << e.what() << event;
 					continue;
 				}
 
-				QUrl url(event.content().value("url").toString());
-				QString body(event.content().value("body").toString());
+				auto with_sender = last_sender_ != notice.sender();
+				auto color = TimelineViewManager::getUserColor(notice.sender());
 
-				auto with_sender = last_sender_ != event.sender();
-				auto color = TimelineViewManager::getUserColor(event.sender());
+				addHistoryItem(notice, color, with_sender);
+				last_sender_ = notice.sender();
 
-				addImageItem(body, url, event, color, with_sender);
+				message_count += 1;
+			} else if (msg_type == events::MessageEventType::Image) {
+				events::MessageEvent<msgs::Image> img;
 
-				last_sender_ = event.sender();
+				try {
+					img.deserialize(event.toObject());
+				} catch (const DeserializationException &e) {
+					qWarning() << e.what() << event;
+					continue;
+				}
+
+				auto with_sender = last_sender_ != img.sender();
+				auto color = TimelineViewManager::getUserColor(img.sender());
+
+				addHistoryItem(img, color, with_sender);
+
+				last_sender_ = img.sender();
 				message_count += 1;
+			} else if (msg_type == events::MessageEventType::Unknown) {
+				qWarning() << "Unknown message type" << event.toObject();
+				continue;
 			}
 		}
 	}
@@ -136,13 +171,9 @@ 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)
+void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender)
 {
-	auto image = new ImageItem(client_, event, body, url);
+	auto image = new ImageItem(client_, event);
 
 	if (with_sender) {
 		auto item = new TimelineItem(image, event, color, scroll_widget_);
@@ -153,7 +184,13 @@ void TimelineView::addImageItem(const QString &body,
 	}
 }
 
-void TimelineView::addHistoryItem(const Event &event, const QString &color, bool with_sender)
+void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Notice> &event, const QString &color, bool with_sender)
+{
+	TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_);
+	scroll_layout_->addWidget(item);
+}
+
+void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Text> &event, const QString &color, bool with_sender)
 {
 	TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_);
 	scroll_layout_->addWidget(item);
@@ -169,34 +206,25 @@ void TimelineView::updatePendingMessage(int txn_id, QString event_id)
 	}
 }
 
-bool TimelineView::isPendingMessage(const Event &event, const QString &userid)
+bool TimelineView::isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &local_userid)
 {
-	if (event.sender() != userid || event.type() != "m.room.message")
-		return false;
-
-	auto msgtype = event.content().value("msgtype").toString();
-	auto body = event.content().value("body").toString();
-
-	// FIXME: should contain more checks later on for other types of messages.
-	if (msgtype != "m.text")
+	if (e.sender() != local_userid)
 		return false;
 
 	for (const auto &msg : pending_msgs_) {
-		if (msg.event_id == event.eventId() || msg.body == body)
+		if (msg.event_id == e.eventId() || msg.body == e.content().body())
 			return true;
 	}
 
 	return false;
 }
 
-void TimelineView::removePendingMessage(const Event &event)
+void TimelineView::removePendingMessage(const events::MessageEvent<msgs::Text> &e)
 {
-	auto body = event.content().value("body").toString();
-
 	for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); it++) {
 		int index = std::distance(pending_msgs_.begin(), it);
 
-		if (it->event_id == event.eventId() || it->body == body) {
+		if (it->event_id == e.eventId() || it->body == e.content().body()) {
 			pending_msgs_.removeAt(index);
 			break;
 		}
diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index ddb142d3..bf3dd997 100644
--- a/src/TimelineViewManager.cc
+++ b/src/TimelineViewManager.cc
@@ -54,11 +54,11 @@ void TimelineViewManager::messageSent(const QString &event_id, const QString &ro
 
 void TimelineViewManager::sendTextMessage(const QString &msg)
 {
-	auto room = active_room_;
-	auto view = views_[room.id()];
+	auto room_id = active_room_;
+	auto view = views_[room_id];
 
 	view->addUserTextMessage(msg, client_->transactionId());
-	client_->sendTextMessage(room.id(), msg);
+	client_->sendTextMessage(room_id, msg);
 }
 
 void TimelineViewManager::clearAll()
@@ -95,7 +95,7 @@ void TimelineViewManager::sync(const Rooms &rooms)
 		auto roomid = it.key();
 
 		if (!views_.contains(roomid)) {
-			qDebug() << "Ignoring event from unknown room";
+			qDebug() << "Ignoring event from unknown room" << roomid;
 			continue;
 		}
 
@@ -105,26 +105,25 @@ void TimelineViewManager::sync(const Rooms &rooms)
 		int msgs_added = view->addEvents(events);
 
 		if (msgs_added > 0) {
-			// TODO: When window gets active the current
+			// TODO: When the app window gets active the current
 			// unread count (if any) should be cleared.
 			auto isAppActive = QApplication::activeWindow() != nullptr;
 
-			if (roomid != active_room_.id() || !isAppActive)
+			if (roomid != active_room_ || !isAppActive)
 				emit unreadMessages(roomid, msgs_added);
 		}
 	}
 }
 
-void TimelineViewManager::setHistoryView(const RoomInfo &info)
+void TimelineViewManager::setHistoryView(const QString &room_id)
 {
-	if (!views_.contains(info.id())) {
-		qDebug() << "Room List id is not present in view manager";
-		qDebug() << info.name();
+	if (!views_.contains(room_id)) {
+		qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id;
 		return;
 	}
 
-	active_room_ = info;
-	auto widget = views_.value(info.id());
+	active_room_ = room_id;
+	auto widget = views_.value(room_id);
 
 	setCurrentWidget(widget);
 }
diff --git a/src/events/Event.cc b/src/events/Event.cc
index 9a8590e0..da4f3e99 100644
--- a/src/events/Event.cc
+++ b/src/events/Event.cc
@@ -50,6 +50,8 @@ matrix::events::EventType matrix::events::extractEventType(const QJsonObject &ob
 		return EventType::RoomJoinRules;
 	else if (type == "m.room.member")
 		return EventType::RoomMember;
+	else if (type == "m.room.message")
+		return EventType::RoomMessage;
 	else if (type == "m.room.name")
 		return EventType::RoomName;
 	else if (type == "m.room.power_levels")
@@ -59,3 +61,22 @@ matrix::events::EventType matrix::events::extractEventType(const QJsonObject &ob
 	else
 		return EventType::Unsupported;
 }
+
+bool matrix::events::isStateEvent(EventType type)
+{
+	return type == EventType::RoomAliases ||
+	       type == EventType::RoomAvatar ||
+	       type == EventType::RoomCanonicalAlias ||
+	       type == EventType::RoomCreate ||
+	       type == EventType::RoomHistoryVisibility ||
+	       type == EventType::RoomJoinRules ||
+	       type == EventType::RoomMember ||
+	       type == EventType::RoomName ||
+	       type == EventType::RoomPowerLevels ||
+	       type == EventType::RoomTopic;
+}
+
+bool matrix::events::isMessageEvent(EventType type)
+{
+	return type == EventType::RoomMessage;
+}
diff --git a/src/events/MessageEventContent.cc b/src/events/MessageEventContent.cc
new file mode 100644
index 00000000..df2c39e8
--- /dev/null
+++ b/src/events/MessageEventContent.cc
@@ -0,0 +1,63 @@
+/*
+ * 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 <QDebug>
+
+#include "MessageEventContent.h"
+
+using namespace matrix::events;
+
+MessageEventType matrix::events::extractMessageEventType(const QJsonObject &data)
+{
+	if (!data.contains("content"))
+		return MessageEventType::Unknown;
+
+	auto content = data.value("content").toObject();
+	auto msgtype = content.value("msgtype").toString();
+
+	if (msgtype == "m.audio")
+		return MessageEventType::Audio;
+	else if (msgtype == "m.emote")
+		return MessageEventType::Emote;
+	else if (msgtype == "m.file")
+		return MessageEventType::File;
+	else if (msgtype == "m.image")
+		return MessageEventType::Image;
+	else if (msgtype == "m.location")
+		return MessageEventType::Location;
+	else if (msgtype == "m.notice")
+		return MessageEventType::Notice;
+	else if (msgtype == "m.text")
+		return MessageEventType::Text;
+	else if (msgtype == "m.video")
+		return MessageEventType::Video;
+	else
+		return MessageEventType::Unknown;
+}
+
+void MessageEventContent::deserialize(const QJsonValue &data)
+{
+	if (!data.isObject())
+		throw DeserializationException("MessageEventContent is not a JSON object");
+
+	auto object = data.toObject();
+
+	if (!object.contains("body"))
+		throw DeserializationException("body key is missing");
+
+	body_ = object.value("body").toString();
+}
diff --git a/src/events/messages/Audio.cc b/src/events/messages/Audio.cc
new file mode 100644
index 00000000..f0fb443b
--- /dev/null
+++ b/src/events/messages/Audio.cc
@@ -0,0 +1,39 @@
+/*
+ * 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 "Audio.h"
+
+using namespace matrix::events::messages;
+
+void Audio::deserialize(const QJsonObject &object)
+{
+	if (!object.contains("url"))
+		throw DeserializationException("url key is missing");
+
+	url_ = object.value("url").toString();
+
+	if (object.value("msgtype") != "m.audio")
+		throw DeserializationException("invalid msgtype for audio");
+
+	if (object.contains("info")) {
+		auto info = object.value("info").toObject();
+
+		info_.duration = info.value("duration").toInt();
+		info_.mimetype = info.value("mimetype").toString();
+		info_.size = info.value("size").toInt();
+	}
+}
diff --git a/src/events/messages/Emote.cc b/src/events/messages/Emote.cc
new file mode 100644
index 00000000..1d6a4753
--- /dev/null
+++ b/src/events/messages/Emote.cc
@@ -0,0 +1,26 @@
+/*
+ * 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 "Emote.h"
+
+using namespace matrix::events::messages;
+
+void Emote::deserialize(const QJsonObject &object)
+{
+	if (object.value("msgtype") != "m.emote")
+		throw DeserializationException("invalid msgtype for emote");
+}
diff --git a/src/events/messages/File.cc b/src/events/messages/File.cc
new file mode 100644
index 00000000..a6b5b6c2
--- /dev/null
+++ b/src/events/messages/File.cc
@@ -0,0 +1,51 @@
+/*
+ * 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 "File.h"
+
+using namespace matrix::events::messages;
+
+void File::deserialize(const QJsonObject &object)
+{
+	if (!object.contains("url"))
+		throw DeserializationException("messages::File url key is missing");
+
+	if (!object.contains("filename"))
+		throw DeserializationException("messages::File filename key is missing");
+
+	if (object.value("msgtype") != "m.file")
+		throw DeserializationException("invalid msgtype for file");
+
+	url_ = object.value("url").toString();
+
+	if (object.contains("info")) {
+		auto file_info = object.value("info").toObject();
+
+		info_.size = file_info.value("size").toInt();
+		info_.mimetype = file_info.value("mimetype").toString();
+		info_.thumbnail_url = file_info.value("thumbnail_url").toString();
+
+		if (file_info.contains("thumbnail_info")) {
+			auto thumbinfo = file_info.value("thumbnail_info").toObject();
+
+			info_.thumbnail_info.h = thumbinfo.value("h").toInt();
+			info_.thumbnail_info.w = thumbinfo.value("w").toInt();
+			info_.thumbnail_info.size = thumbinfo.value("size").toInt();
+			info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString();
+		}
+	}
+}
diff --git a/src/events/messages/Image.cc b/src/events/messages/Image.cc
new file mode 100644
index 00000000..d528e174
--- /dev/null
+++ b/src/events/messages/Image.cc
@@ -0,0 +1,51 @@
+/*
+ * 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 "Image.h"
+
+using namespace matrix::events::messages;
+
+void Image::deserialize(const QJsonObject &object)
+{
+	if (!object.contains("url"))
+		throw DeserializationException("messages::Image url key is missing");
+
+	url_ = object.value("url").toString();
+
+	if (object.value("msgtype") != "m.image")
+		throw DeserializationException("invalid msgtype for image");
+
+	if (object.contains("info")) {
+		auto imginfo = object.value("info").toObject();
+
+		info_.w = imginfo.value("w").toInt();
+		info_.h = imginfo.value("h").toInt();
+		info_.size = imginfo.value("size").toInt();
+
+		info_.mimetype = imginfo.value("mimetype").toString();
+		info_.thumbnail_url = imginfo.value("thumbnail_url").toString();
+
+		if (imginfo.contains("thumbnail_info")) {
+			auto thumbinfo = imginfo.value("thumbnail_info").toObject();
+
+			info_.thumbnail_info.h = thumbinfo.value("h").toInt();
+			info_.thumbnail_info.w = thumbinfo.value("w").toInt();
+			info_.thumbnail_info.size = thumbinfo.value("size").toInt();
+			info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString();
+		}
+	}
+}
diff --git a/src/events/messages/Location.cc b/src/events/messages/Location.cc
new file mode 100644
index 00000000..68a9a9c1
--- /dev/null
+++ b/src/events/messages/Location.cc
@@ -0,0 +1,46 @@
+/*
+ * 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 "Location.h"
+
+using namespace matrix::events::messages;
+
+void Location::deserialize(const QJsonObject &object)
+{
+	if (!object.contains("geo_uri"))
+		throw DeserializationException("messages::Location geo_uri key is missing");
+
+	if (object.value("msgtype") != "m.location")
+		throw DeserializationException("invalid msgtype for location");
+
+	geo_uri_ = object.value("geo_uri").toString();
+
+	if (object.contains("info")) {
+		auto location_info = object.value("info").toObject();
+
+		info_.thumbnail_url = location_info.value("thumbnail_url").toString();
+
+		if (location_info.contains("thumbnail_info")) {
+			auto thumbinfo = location_info.value("thumbnail_info").toObject();
+
+			info_.thumbnail_info.h = thumbinfo.value("h").toInt();
+			info_.thumbnail_info.w = thumbinfo.value("w").toInt();
+			info_.thumbnail_info.size = thumbinfo.value("size").toInt();
+			info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString();
+		}
+	}
+}
diff --git a/src/events/messages/Notice.cc b/src/events/messages/Notice.cc
new file mode 100644
index 00000000..1dd4cc28
--- /dev/null
+++ b/src/events/messages/Notice.cc
@@ -0,0 +1,26 @@
+/*
+ * 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 "Notice.h"
+
+using namespace matrix::events::messages;
+
+void Notice::deserialize(const QJsonObject &object)
+{
+	if (object.value("msgtype") != "m.notice")
+		throw DeserializationException("invalid msgtype for notice");
+}
diff --git a/src/events/messages/Text.cc b/src/events/messages/Text.cc
new file mode 100644
index 00000000..5446d7f4
--- /dev/null
+++ b/src/events/messages/Text.cc
@@ -0,0 +1,26 @@
+/*
+ * 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 "Text.h"
+
+using namespace matrix::events::messages;
+
+void Text::deserialize(const QJsonObject &object)
+{
+	if (object.value("msgtype") != "m.text")
+		throw DeserializationException("invalid msgtype for text");
+}
diff --git a/src/events/messages/Video.cc b/src/events/messages/Video.cc
new file mode 100644
index 00000000..a7ddba96
--- /dev/null
+++ b/src/events/messages/Video.cc
@@ -0,0 +1,52 @@
+/*
+ * 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 "Video.h"
+
+using namespace matrix::events::messages;
+
+void Video::deserialize(const QJsonObject &object)
+{
+	if (!object.contains("url"))
+		throw DeserializationException("messages::Video url key is missing");
+
+	url_ = object.value("url").toString();
+
+	if (object.value("msgtype") != "m.video")
+		throw DeserializationException("invalid msgtype for video");
+
+	if (object.contains("info")) {
+		auto video_info = object.value("info").toObject();
+
+		info_.w = video_info.value("w").toInt();
+		info_.h = video_info.value("h").toInt();
+		info_.size = video_info.value("size").toInt();
+		info_.duration = video_info.value("duration").toInt();
+
+		info_.mimetype = video_info.value("mimetype").toString();
+		info_.thumbnail_url = video_info.value("thumbnail_url").toString();
+
+		if (video_info.contains("thumbnail_info")) {
+			auto thumbinfo = video_info.value("thumbnail_info").toObject();
+
+			info_.thumbnail_info.h = thumbinfo.value("h").toInt();
+			info_.thumbnail_info.w = thumbinfo.value("w").toInt();
+			info_.thumbnail_info.size = thumbinfo.value("size").toInt();
+			info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString();
+		}
+	}
+}
diff --git a/tests/event_collection.cc b/tests/event_collection.cc
new file mode 100644
index 00000000..40b9ff13
--- /dev/null
+++ b/tests/event_collection.cc
@@ -0,0 +1,115 @@
+#include <gtest/gtest.h>
+
+#include <QJsonArray>
+#include <QJsonObject>
+
+#include "Event.h"
+#include "RoomEvent.h"
+#include "StateEvent.h"
+
+#include "AliasesEventContent.h"
+#include "AvatarEventContent.h"
+#include "CanonicalAliasEventContent.h"
+#include "CreateEventContent.h"
+#include "HistoryVisibilityEventContent.h"
+#include "JoinRulesEventContent.h"
+#include "MemberEventContent.h"
+#include "NameEventContent.h"
+#include "PowerLevelsEventContent.h"
+#include "TopicEventContent.h"
+
+using namespace matrix::events;
+
+TEST(EventCollection, Deserialize)
+{
+	auto events = QJsonArray{
+		QJsonObject{
+			{"content", QJsonObject{{"name", "Name"}}},
+			{"event_id", "$asdfafdf8af:matrix.org"},
+			{"prev_content", QJsonObject{{"name", "Previous Name"}}},
+			{"room_id", "!aasdfaeae23r9:matrix.org"},
+			{"sender", "@alice:matrix.org"},
+			{"origin_server_ts", 1323238293289323LL},
+			{"state_key", ""},
+			{"type", "m.room.name"}},
+		QJsonObject{
+			{"content", QJsonObject{{"topic", "Topic"}}},
+			{"event_id", "$asdfafdf8af:matrix.org"},
+			{"prev_content", QJsonObject{{"topic", "Previous Topic"}}},
+			{"room_id", "!aasdfaeae23r9:matrix.org"},
+			{"state_key", ""},
+			{"sender", "@alice:matrix.org"},
+			{"origin_server_ts", 1323238293289323LL},
+			{"type", "m.room.topic"}},
+	};
+
+	for (const auto &event : events) {
+		EventType ty = extractEventType(event.toObject());
+
+		if (ty == EventType::RoomName) {
+			StateEvent<NameEventContent> name_event;
+			name_event.deserialize(event);
+
+			EXPECT_EQ(name_event.content().name(), "Name");
+			EXPECT_EQ(name_event.previousContent().name(), "Previous Name");
+		} else if (ty == EventType::RoomTopic) {
+			StateEvent<TopicEventContent> topic_event;
+			topic_event.deserialize(event);
+
+			EXPECT_EQ(topic_event.content().topic(), "Topic");
+			EXPECT_EQ(topic_event.previousContent().topic(), "Previous Topic");
+		} else {
+			ASSERT_EQ(false, true);
+		}
+	}
+}
+
+TEST(EventCollection, DeserializationException)
+{
+	// Using wrong event types.
+	auto events = QJsonArray{
+		QJsonObject{
+			{"content", QJsonObject{{"name", "Name"}}},
+			{"event_id", "$asdfafdf8af:matrix.org"},
+			{"prev_content", QJsonObject{{"name", "Previous Name"}}},
+			{"room_id", "!aasdfaeae23r9:matrix.org"},
+			{"sender", "@alice:matrix.org"},
+			{"origin_server_ts", 1323238293289323LL},
+			{"state_key", ""},
+			{"type", "m.room.topic"}},
+		QJsonObject{
+			{"content", QJsonObject{{"topic", "Topic"}}},
+			{"event_id", "$asdfafdf8af:matrix.org"},
+			{"prev_content", QJsonObject{{"topic", "Previous Topic"}}},
+			{"room_id", "!aasdfaeae23r9:matrix.org"},
+			{"state_key", ""},
+			{"sender", "@alice:matrix.org"},
+			{"origin_server_ts", 1323238293289323LL},
+			{"type", "m.room.name"}},
+	};
+
+	for (const auto &event : events) {
+		EventType ty = extractEventType(event.toObject());
+
+		if (ty == EventType::RoomName) {
+			StateEvent<NameEventContent> name_event;
+
+			try {
+				name_event.deserialize(event);
+			} catch (const DeserializationException &e) {
+				ASSERT_STREQ("name key is missing", e.what());
+			}
+
+		} else if (ty == EventType::RoomTopic) {
+			StateEvent<TopicEventContent> topic_event;
+
+			try {
+				topic_event.deserialize(event);
+			} catch (const DeserializationException &e) {
+				ASSERT_STREQ("topic key is missing", e.what());
+			}
+		} else {
+			ASSERT_EQ(false, true);
+		}
+	}
+}
diff --git a/tests/events.cc b/tests/events.cc
index c3f8f7b7..cb8b8089 100644
--- a/tests/events.cc
+++ b/tests/events.cc
@@ -183,6 +183,7 @@ TEST(EventType, Mapping)
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.history_visibility"}}), EventType::RoomHistoryVisibility);
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.join_rules"}}), EventType::RoomJoinRules);
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.member"}}), EventType::RoomMember);
+	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.message"}}), EventType::RoomMessage);
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.name"}}), EventType::RoomName);
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.power_levels"}}), EventType::RoomPowerLevels);
 	EXPECT_EQ(extractEventType(QJsonObject{{"type", "m.room.topic"}}), EventType::RoomTopic);
diff --git a/tests/message_events.cc b/tests/message_events.cc
new file mode 100644
index 00000000..cece67db
--- /dev/null
+++ b/tests/message_events.cc
@@ -0,0 +1,311 @@
+#include <gtest/gtest.h>
+
+#include <QJsonArray>
+#include <QJsonObject>
+
+#include "MessageEvent.h"
+#include "MessageEventContent.h"
+
+#include "Audio.h"
+#include "Emote.h"
+#include "File.h"
+#include "Image.h"
+#include "Location.h"
+#include "Notice.h"
+#include "Text.h"
+#include "Video.h"
+
+using namespace matrix::events;
+
+TEST(MessageEvent, Audio)
+{
+	auto info = QJsonObject{
+		{"duration", 2140786},
+		{"mimetype", "audio/mpeg"},
+		{"size", 1563688}};
+
+	auto content = QJsonObject{
+		{"body", "Bee Gees - Stayin' Alive"},
+		{"msgtype", "m.audio"},
+		{"url", "mxc://localhost/2sdfj23f33r3faad"},
+		{"info", info}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Audio> audio;
+	audio.deserialize(event);
+
+	EXPECT_EQ(audio.msgContent().info().duration, 2140786);
+	EXPECT_EQ(audio.msgContent().info().size, 1563688);
+	EXPECT_EQ(audio.msgContent().info().mimetype, "audio/mpeg");
+	EXPECT_EQ(audio.content().body(), "Bee Gees - Stayin' Alive");
+}
+
+TEST(MessageEvent, Emote)
+{
+	auto content = QJsonObject{
+		{"body", "emote message"},
+		{"msgtype", "m.emote"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Emote> emote;
+	emote.deserialize(event);
+
+	EXPECT_EQ(emote.content().body(), "emote message");
+}
+
+TEST(MessageEvent, File)
+{
+	auto thumbnail_info = QJsonObject{
+		{"h", 300},
+		{"w", 400},
+		{"size", 3432434},
+		{"mimetype", "image/jpeg"}};
+
+	auto file_info = QJsonObject{
+		{"size", 24242424},
+		{"mimetype", "application/msword"},
+		{"thumbnail_url", "mxc://localhost/adfaefaFAFSDFF3"},
+		{"thumbnail_info", thumbnail_info}};
+
+	auto content = QJsonObject{
+		{"body", "something-important.doc"},
+		{"filename", "something-important.doc"},
+		{"url", "mxc://localhost/23d233d32r3r2r"},
+		{"info", file_info},
+		{"msgtype", "m.file"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::File> file;
+	file.deserialize(event);
+
+	EXPECT_EQ(file.content().body(), "something-important.doc");
+	EXPECT_EQ(file.msgContent().info().thumbnail_info.h, 300);
+	EXPECT_EQ(file.msgContent().info().thumbnail_info.w, 400);
+	EXPECT_EQ(file.msgContent().info().thumbnail_info.mimetype, "image/jpeg");
+	EXPECT_EQ(file.msgContent().info().mimetype, "application/msword");
+	EXPECT_EQ(file.msgContent().info().size, 24242424);
+	EXPECT_EQ(file.content().body(), "something-important.doc");
+}
+
+TEST(MessageEvent, Image)
+{
+	auto thumbinfo = QJsonObject{
+		{"h", 11},
+		{"w", 22},
+		{"size", 212},
+		{"mimetype", "img/jpeg"},
+	};
+
+	auto imginfo = QJsonObject{
+		{"h", 110},
+		{"w", 220},
+		{"size", 2120},
+		{"mimetype", "img/jpeg"},
+		{"thumbnail_url", "https://images.com/image-thumb.jpg"},
+		{"thumbnail_info", thumbinfo},
+	};
+
+	auto content = QJsonObject{
+		{"body", "Image title"},
+		{"msgtype", "m.image"},
+		{"url", "https://images.com/image.jpg"},
+		{"info", imginfo}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Image> img;
+	img.deserialize(event);
+
+	EXPECT_EQ(img.content().body(), "Image title");
+	EXPECT_EQ(img.msgContent().info().h, 110);
+	EXPECT_EQ(img.msgContent().info().w, 220);
+	EXPECT_EQ(img.msgContent().info().thumbnail_info.w, 22);
+	EXPECT_EQ(img.msgContent().info().mimetype, "img/jpeg");
+	EXPECT_EQ(img.msgContent().info().thumbnail_url, "https://images.com/image-thumb.jpg");
+}
+
+TEST(MessageEvent, Location)
+{
+	auto thumbnail_info = QJsonObject{
+		{"h", 300},
+		{"w", 400},
+		{"size", 3432434},
+		{"mimetype", "image/jpeg"}};
+
+	auto info = QJsonObject{
+		{"thumbnail_url", "mxc://localhost/adfaefaFAFSDFF3"},
+		{"thumbnail_info", thumbnail_info}};
+
+	auto content = QJsonObject{
+		{"body", "Big Ben, London, UK"},
+		{"geo_uri", "geo:51.5008,0.1247"},
+		{"info", info},
+		{"msgtype", "m.location"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Location> location;
+	location.deserialize(event);
+
+	EXPECT_EQ(location.msgContent().info().thumbnail_info.h, 300);
+	EXPECT_EQ(location.msgContent().info().thumbnail_info.w, 400);
+	EXPECT_EQ(location.msgContent().info().thumbnail_info.mimetype, "image/jpeg");
+	EXPECT_EQ(location.msgContent().info().thumbnail_url, "mxc://localhost/adfaefaFAFSDFF3");
+	EXPECT_EQ(location.content().body(), "Big Ben, London, UK");
+}
+
+TEST(MessageEvent, Notice)
+{
+	auto content = QJsonObject{
+		{"body", "notice message"},
+		{"msgtype", "m.notice"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Notice> notice;
+	notice.deserialize(event);
+
+	EXPECT_EQ(notice.content().body(), "notice message");
+}
+
+TEST(MessageEvent, Text)
+{
+	auto content = QJsonObject{
+		{"body", "text message"},
+		{"msgtype", "m.text"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Text> text;
+	text.deserialize(event);
+
+	EXPECT_EQ(text.content().body(), "text message");
+}
+
+TEST(MessageEvent, Video)
+{
+	auto thumbnail_info = QJsonObject{
+		{"h", 300},
+		{"w", 400},
+		{"size", 3432434},
+		{"mimetype", "image/jpeg"}};
+
+	auto video_info = QJsonObject{
+		{"h", 222},
+		{"w", 333},
+		{"duration", 232323},
+		{"size", 24242424},
+		{"mimetype", "video/mp4"},
+		{"thumbnail_url", "mxc://localhost/adfaefaFAFSDFF3"},
+		{"thumbnail_info", thumbnail_info}};
+
+	auto content = QJsonObject{
+		{"body", "Gangnam Style"},
+		{"url", "mxc://localhost/23d233d32r3r2r"},
+		{"info", video_info},
+		{"msgtype", "m.video"}};
+
+	auto event = QJsonObject{
+		{"content", content},
+		{"event_id", "$asdfafdf8af:matrix.org"},
+		{"room_id", "!aasdfaeae23r9:matrix.org"},
+		{"sender", "@alice:matrix.org"},
+		{"origin_server_ts", 1323238293289323LL},
+		{"type", "m.room.message"}};
+
+	MessageEvent<messages::Video> video;
+	video.deserialize(event);
+
+	EXPECT_EQ(video.msgContent().info().thumbnail_info.h, 300);
+	EXPECT_EQ(video.msgContent().info().thumbnail_info.w, 400);
+	EXPECT_EQ(video.msgContent().info().thumbnail_info.mimetype, "image/jpeg");
+	EXPECT_EQ(video.msgContent().info().duration, 232323);
+	EXPECT_EQ(video.msgContent().info().size, 24242424);
+	EXPECT_EQ(video.msgContent().info().mimetype, "video/mp4");
+	EXPECT_EQ(video.content().body(), "Gangnam Style");
+}
+
+TEST(MessageEvent, Types)
+{
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.audio"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Audio);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.emote"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Emote);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.file"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::File);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.image"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Image);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.location"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Location);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.notice"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Notice);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.text"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Text);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.video"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Video);
+	EXPECT_EQ(extractMessageEventType(QJsonObject{
+			  {"content", QJsonObject{{"msgtype", "m.random"}}}, {"type", "m.room.message"},
+		  }),
+		  MessageEventType::Unknown);
+}