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.
This commit is contained in:
Konstantinos Sideris 2017-05-07 17:15:38 +03:00
parent 8825e072f2
commit 1f90c58076
45 changed files with 1922 additions and 303 deletions

View File

@ -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)

View File

@ -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_;
};

View File

@ -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_;
};

View File

@ -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)

View File

@ -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 setInitialRooms(const QMap<QString, RoomState> &states);
void sync(const QMap<QString, RoomState> &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:

63
include/RoomState.h Normal file
View File

@ -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

View File

@ -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_;
}

View File

@ -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();

View File

@ -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_;

View File

@ -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_;
};

View File

@ -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
{

View File

@ -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

View File

@ -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

View File

@ -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");

View File

@ -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>
#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

View File

@ -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 <QList>
#include <QString>
#include <QUrl>
#include <QJsonObject>
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<QString> aliases_;
void deserialize(const QJsonObject &obj) override;
};
} // namespace messages
} // namespace events
} // namespace matrix
#endif // ROOM_INFO_H
#endif // MESSAGE_EVENT_EMOTE_H

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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) {

View File

@ -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)

View File

@ -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);

View File

@ -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());
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<QString, RoomState> &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);
}
}

32
src/RoomState.cc Normal file
View File

@ -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";
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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,53 +61,80 @@ 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);
if (ty == events::RoomMessage) {
events::MessageEventType msg_type = events::extractMessageEventType(event.toObject());
if (msg_type == events::MessageEventType::Text) {
events::MessageEvent<msgs::Text> 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<msgs::Notice> 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<msgs::Image> 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<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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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");
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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");
}

View File

@ -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");
}

View File

@ -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();
}
}
}

115
tests/event_collection.cc Normal file
View File

@ -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);
}
}
}

View File

@ -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);

311
tests/message_events.cc Normal file
View File

@ -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);
}