From 1f90c58076b9c40a609c88b9cdad51dd55da6954 Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Sun, 7 May 2017 17:15:38 +0300 Subject: [PATCH] Use timeline to retrieve state events - Rooms without any history will be shown. - Room's state will be kept in sync and any updates will be visible. --- CMakeLists.txt | 21 +- include/ChatPage.h | 10 +- include/ImageItem.h | 10 +- include/RoomInfoListItem.h | 24 +- include/RoomList.h | 12 +- include/RoomState.h | 63 ++++ include/Sync.h | 13 +- include/TimelineItem.h | 17 +- include/TimelineView.h | 22 +- include/TimelineViewManager.h | 5 +- include/events/Event.h | 5 + include/events/MessageEvent.h | 67 ++++ include/events/MessageEventContent.h | 78 +++++ include/events/RoomEvent.h | 5 +- .../events/messages/Audio.h | 86 +++-- .../{RoomInfo.h => events/messages/Emote.h} | 44 +-- include/events/messages/File.h | 76 +++++ include/events/messages/Image.h | 69 ++++ include/events/messages/Location.h | 65 ++++ include/events/messages/Notice.h | 40 +++ include/events/messages/Text.h | 40 +++ include/events/messages/Video.h | 70 ++++ src/ChatPage.cc | 160 +++++++-- src/ImageItem.cc | 10 +- src/MatrixClient.cc | 4 +- src/RoomInfoListItem.cc | 36 +- src/RoomList.cc | 86 +++-- src/RoomState.cc | 32 ++ src/Sync.cc | 28 +- src/TimelineItem.cc | 30 +- src/TimelineView.cc | 146 ++++---- src/TimelineViewManager.cc | 23 +- src/events/Event.cc | 21 ++ src/events/MessageEventContent.cc | 63 ++++ src/events/messages/Audio.cc | 39 +++ src/events/messages/Emote.cc | 26 ++ src/events/messages/File.cc | 51 +++ src/events/messages/Image.cc | 51 +++ src/events/messages/Location.cc | 46 +++ src/events/messages/Notice.cc | 26 ++ src/events/messages/Text.cc | 26 ++ src/events/messages/Video.cc | 52 +++ tests/event_collection.cc | 115 +++++++ tests/events.cc | 1 + tests/message_events.cc | 311 ++++++++++++++++++ 45 files changed, 1922 insertions(+), 303 deletions(-) create mode 100644 include/RoomState.h create mode 100644 include/events/MessageEvent.h create mode 100644 include/events/MessageEventContent.h rename src/RoomInfo.cc => include/events/messages/Audio.h (53%) rename include/{RoomInfo.h => events/messages/Emote.h} (57%) create mode 100644 include/events/messages/File.h create mode 100644 include/events/messages/Image.h create mode 100644 include/events/messages/Location.h create mode 100644 include/events/messages/Notice.h create mode 100644 include/events/messages/Text.h create mode 100644 include/events/messages/Video.h create mode 100644 src/RoomState.cc create mode 100644 src/events/MessageEventContent.cc create mode 100644 src/events/messages/Audio.cc create mode 100644 src/events/messages/Emote.cc create mode 100644 src/events/messages/File.cc create mode 100644 src/events/messages/Image.cc create mode 100644 src/events/messages/Location.cc create mode 100644 src/events/messages/Notice.cc create mode 100644 src/events/messages/Text.cc create mode 100644 src/events/messages/Video.cc create mode 100644 tests/event_collection.cc create mode 100644 tests/message_events.cc 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 #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 room_avatars_; UserInfoWidget *user_info_widget_; + QMap state_manager_; + // Matrix Client API provider. QSharedPointer 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 #include +#include "Image.h" #include "MatrixClient.h" +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + class ImageItem : public QWidget { Q_OBJECT public: ImageItem(QSharedPointer client, - const Event &event, - const QString &body, - const QUrl &url, + const events::MessageEvent &event, QWidget *parent = nullptr); void setImage(const QPixmap &image); @@ -65,7 +67,7 @@ private: int bottom_height_ = 30; - Event event_; + events::MessageEvent event_; QSharedPointer 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 #include "MatrixClient.h" -#include "RoomInfo.h" #include "RoomInfoListItem.h" +#include "RoomState.h" #include "Sync.h" namespace Ui @@ -41,18 +41,18 @@ public: RoomList(QSharedPointer client, QWidget *parent = 0); ~RoomList(); - void setInitialRooms(const Rooms &rooms); + void setInitialRooms(const QMap &states); + void sync(const QMap &states); + void clear(); - RoomInfo extractRoomInfo(const State &room_state); - 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 + * + * 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 . + */ + +#ifndef ROOM_STATE_H +#define ROOM_STATE_H + +#include + +#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 aliases; + events::StateEvent avatar; + events::StateEvent canonical_alias; + events::StateEvent create; + events::StateEvent history_visibility; + events::StateEvent join_rules; + events::StateEvent name; + events::StateEvent power_levels; + events::StateEvent 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 #include #include #include @@ -90,13 +91,13 @@ class State : public Deserializable { public: void deserialize(const QJsonValue &data) override; - inline QList events() const; + inline QJsonArray events() const; private: - QList events_; + QJsonArray events_; }; -inline QList State::events() const +inline QJsonArray State::events() const { return events_; } @@ -104,19 +105,19 @@ inline QList State::events() const class Timeline : public Deserializable { public: - inline QList events() const; + inline QJsonArray events() const; inline QString previousBatch() const; inline bool limited() const; void deserialize(const QJsonValue &data) override; private: - QList events_; + QJsonArray events_; QString prev_batch_; bool limited_; }; -inline QList 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 &e, bool with_sender, const QString &color, QWidget *parent = 0); + TimelineItem(const events::MessageEvent &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 &e, const QString &color, QWidget *parent); + TimelineItem(ImageItem *img, const events::MessageEvent &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 client, QWidget *parent = 0); - TimelineView(const QList &events, QSharedPointer client, QWidget *parent = 0); + TimelineView(const QJsonArray &events, QSharedPointer 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 &events); + void addHistoryItem(const events::MessageEvent &e, const QString &color, bool with_sender); + void addHistoryItem(const events::MessageEvent &e, const QString &color, bool with_sender); + void addHistoryItem(const events::MessageEvent &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 &e); + bool isPendingMessage(const events::MessageEvent &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 #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 views_; QSharedPointer 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 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 + * + * 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 . + */ + +#ifndef MATRIX_MESSAGE_EVENT_H +#define MATRIX_MESSAGE_EVENT_H + +#include "MessageEventContent.h" +#include "RoomEvent.h" + +namespace matrix +{ +namespace events +{ +template +class MessageEvent : public RoomEvent +{ +public: + inline MsgContent msgContent() const; + + void deserialize(const QJsonValue &data) override; + +private: + MsgContent msg_content_; +}; + +template +inline MsgContent MessageEvent::msgContent() const +{ + return msg_content_; +} + +template +void MessageEvent::deserialize(const QJsonValue &data) +{ + RoomEvent::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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_CONTENT_H +#define MESSAGE_EVENT_CONTENT_H + +#include + +#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::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 similarity index 53% rename from src/RoomInfo.cc rename to 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 . */ -#include "RoomInfo.h" +#ifndef MESSAGE_EVENT_AUDIO_H +#define MESSAGE_EVENT_AUDIO_H -RoomInfo::RoomInfo() - : name_("") - , topic_("") +#include + +#include "Deserializable.h" + +namespace matrix { +namespace events +{ +namespace messages +{ +struct AudioInfo { + uint64_t duration; + int size; + + QString mimetype; +}; + +class Audio : public Deserializable +{ +public: + inline QString url() const; + inline AudioInfo info() const; + + void deserialize(const QJsonObject &object) override; + +private: + QString url_; + AudioInfo info_; +}; + +inline QString Audio::url() const +{ + return url_; } -RoomInfo::RoomInfo(QString name, QString topic, QUrl avatar_url) - : name_(name) - , topic_(topic) - , avatar_url_(avatar_url) +inline AudioInfo Audio::info() const { + return info_; } -QString RoomInfo::id() const -{ - return id_; -} +} // namespace messages +} // namespace events +} // namespace matrix -QString RoomInfo::name() const -{ - return name_; -} - -QString RoomInfo::topic() const -{ - return topic_; -} - -QUrl RoomInfo::avatarUrl() const -{ - return avatar_url_; -} - -void RoomInfo::setAvatarUrl(const QUrl &url) -{ - avatar_url_ = url; -} - -void RoomInfo::setId(const QString &id) -{ - id_ = id; -} - -void RoomInfo::setName(const QString &name) -{ - name_ = name; -} - -void RoomInfo::setTopic(const QString &topic) -{ - topic_ = topic; -} +#endif // MESSAGE_EVENT_AUDIO_H diff --git a/include/RoomInfo.h b/include/events/messages/Emote.h similarity index 57% rename from include/RoomInfo.h rename to 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 * @@ -15,35 +16,26 @@ * along with this program. If not, see . */ -#ifndef ROOM_INFO_H -#define ROOM_INFO_H +#ifndef MESSAGE_EVENT_EMOTE_H +#define MESSAGE_EVENT_EMOTE_H -#include -#include -#include +#include -class RoomInfo +#include "Deserializable.h" + +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 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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_FILE_H +#define MESSAGE_EVENT_FILE_H + +#include + +#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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_IMAGE_H +#define MESSAGE_EVENT_IMAGE_H + +#include + +#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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_LOCATION_H +#define MESSAGE_EVENT_LOCATION_H + +#include + +#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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_NOTICE_H +#define MESSAGE_EVENT_NOTICE_H + +#include + +#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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_TEXT_H +#define MESSAGE_EVENT_TEXT_H + +#include + +#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 + * + * 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 . + */ + +#ifndef MESSAGE_EVENT_VIDEO_H +#define MESSAGE_EVENT_VIDEO_H + +#include + +#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 client, QWidget *parent) : QWidget(parent) , ui(new Ui::ChatPage) @@ -55,16 +69,9 @@ ChatPage::ChatPage(QSharedPointer 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 aliases; + aliases.deserialize(event); + room_state.aliases = aliases; + break; + } + case events::EventType::RoomAvatar: { + events::StateEvent avatar; + avatar.deserialize(event); + room_state.avatar = avatar; + break; + } + case events::EventType::RoomCanonicalAlias: { + events::StateEvent canonical_alias; + canonical_alias.deserialize(event); + room_state.canonical_alias = canonical_alias; + break; + } + case events::EventType::RoomCreate: { + events::StateEvent create; + create.deserialize(event); + room_state.create = create; + break; + } + case events::EventType::RoomHistoryVisibility: { + events::StateEvent history_visibility; + history_visibility.deserialize(event); + room_state.history_visibility = history_visibility; + break; + } + case events::EventType::RoomJoinRules: { + events::StateEvent join_rules; + join_rules.deserialize(event); + room_state.join_rules = join_rules; + break; + } + case events::EventType::RoomName: { + events::StateEvent name; + name.deserialize(event); + room_state.name = name; + break; + } + case events::EventType::RoomPowerLevels: { + events::StateEvent power_levels; + power_levels.deserialize(event); + room_state.power_levels = power_levels; + break; + } + case events::EventType::RoomTopic: { + events::StateEvent 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 client, const Event &event, const QString &body, const QUrl &url, QWidget *parent) +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + +ImageItem::ImageItem(QSharedPointer client, const events::MessageEvent &event, QWidget *parent) : QWidget(parent) - , url_{url} - , text_{body} , event_{event} , client_{client} { @@ -37,6 +38,9 @@ ImageItem::ImageItem(QSharedPointer client, const Event &event, co setCursor(Qt::PointingHandCursor); setAttribute(Qt::WA_Hover, true); + url_ = event.msgContent().url(); + text_ = event.content().body(); + QList 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 #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 &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()); + for (auto it = states.constBegin(); it != states.constEnd(); it++) { + auto room_id = it.key(); + auto state = it.value(); - if (info.name().isEmpty()) - continue; + if (!state.avatar.content().url().toString().isEmpty()) + client_->fetchRoomAvatar(room_id, state.avatar.content().url()); - if (!info.avatarUrl().isEmpty()) - client_->fetchRoomAvatar(info.id(), info.avatarUrl()); + RoomInfoListItem *room_item = new RoomInfoListItem(state, room_id, ui->scrollArea); + connect(room_item, &RoomInfoListItem::clicked, this, &RoomList::highlightSelectedRoom); - RoomInfoListItem *room_item = new RoomInfoListItem(info, ui->scrollArea); - connect(room_item, - SIGNAL(clicked(const RoomInfo &)), - this, - SLOT(highlightSelectedRoom(const RoomInfo &))); - - 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::highlightSelectedRoom(const RoomInfo &info) +void RoomList::sync(const QMap &states) { - emit roomChanged(info); + for (auto it = states.constBegin(); it != states.constEnd(); it++) { + auto room_id = it.key(); + auto state = it.value(); - if (!rooms_.contains(info.id())) { + // 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 QString &room_id) +{ + emit roomChanged(room_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 + * + * 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 . + */ + +#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 &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 &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 &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 = "" + body + ""; + body = "" + body + ""; + + if (with_sender) + generateBody(event.sender(), color, body); + else + generateBody(body); + + setupLayout(); +} + +TimelineItem::TimelineItem(const events::MessageEvent &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 +#include #include #include #include #include +#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 &events, QSharedPointer client, QWidget *parent) +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + +TimelineView::TimelineView(const QJsonArray &events, QSharedPointer client, QWidget *parent) : QWidget(parent) , client_{client} { @@ -53,53 +61,80 @@ void TimelineView::sliderRangeChanged(int min, int max) scroll_area_->verticalScrollBar()->setValue(max); } -int TimelineView::addEvents(const QList &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); + if (ty == events::RoomMessage) { + events::MessageEventType msg_type = events::extractMessageEventType(event.toObject()); + + if (msg_type == events::MessageEventType::Text) { + events::MessageEvent text; + + try { + text.deserialize(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } + + if (isPendingMessage(text, local_user)) { + removePendingMessage(text); + continue; + } + + 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 notice; + + try { + notice.deserialize(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } + + auto with_sender = last_sender_ != notice.sender(); + auto color = TimelineViewManager::getUserColor(notice.sender()); + + addHistoryItem(notice, color, with_sender); + last_sender_ = notice.sender(); + + message_count += 1; + } else if (msg_type == events::MessageEventType::Image) { + events::MessageEvent img; + + 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; } - - if (msg_type == "m.text" || msg_type == "m.notice") { - auto with_sender = last_sender_ != event.sender(); - auto color = TimelineViewManager::getUserColor(event.sender()); - - addHistoryItem(event, color, with_sender); - last_sender_ = event.sender(); - - message_count += 1; - } else if (msg_type == "m.image") { - // TODO: Move this into serialization. - if (!event.content().contains("url")) { - qWarning() << "Missing url from m.image event" << event.content(); - continue; - } - - if (!event.content().contains("body")) { - qWarning() << "Missing body from m.image event" << event.content(); - continue; - } - - QUrl url(event.content().value("url").toString()); - QString body(event.content().value("body").toString()); - - auto with_sender = last_sender_ != event.sender(); - auto color = TimelineViewManager::getUserColor(event.sender()); - - addImageItem(body, url, event, color, with_sender); - - last_sender_ = event.sender(); - message_count += 1; - } } } @@ -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 &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 &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 &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 &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 &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 + * + * 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 . + */ + +#include + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + * + * 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 . + */ + +#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 + +#include +#include + +#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 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 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 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 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 + +#include +#include + +#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 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 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 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 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 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 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 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 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); +}