summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorKonstantinos Sideris <sideris.konstantin@gmail.com>2017-05-07 17:15:38 +0300
committerKonstantinos Sideris <sideris.konstantin@gmail.com>2017-05-07 17:15:38 +0300
commit1f90c58076b9c40a609c88b9cdad51dd55da6954 (patch)
tree5a45ae1b5b1ca62cf8665aa892de904481dac215 /src
parentAdd matrix::events namespace (diff)
downloadnheko-1f90c58076b9c40a609c88b9cdad51dd55da6954.tar.xz
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.
Diffstat (limited to 'src')
-rw-r--r--src/ChatPage.cc160
-rw-r--r--src/ImageItem.cc10
-rw-r--r--src/MatrixClient.cc4
-rw-r--r--src/RoomInfoListItem.cc36
-rw-r--r--src/RoomList.cc86
-rw-r--r--src/RoomState.cc32
-rw-r--r--src/Sync.cc28
-rw-r--r--src/TimelineItem.cc30
-rw-r--r--src/TimelineView.cc122
-rw-r--r--src/TimelineViewManager.cc23
-rw-r--r--src/events/Event.cc21
-rw-r--r--src/events/MessageEventContent.cc63
-rw-r--r--src/events/messages/Audio.cc (renamed from src/RoomInfo.cc)60
-rw-r--r--src/events/messages/Emote.cc26
-rw-r--r--src/events/messages/File.cc51
-rw-r--r--src/events/messages/Image.cc51
-rw-r--r--src/events/messages/Location.cc46
-rw-r--r--src/events/messages/Notice.cc26
-rw-r--r--src/events/messages/Text.cc26
-rw-r--r--src/events/messages/Video.cc52
20 files changed, 736 insertions, 217 deletions
diff --git a/src/ChatPage.cc b/src/ChatPage.cc

index 0ddf0f8b..fbaf9ddd 100644 --- a/src/ChatPage.cc +++ b/src/ChatPage.cc
@@ -25,6 +25,20 @@ #include "Sync.h" #include "UserInfoWidget.h" +#include "AliasesEventContent.h" +#include "AvatarEventContent.h" +#include "CanonicalAliasEventContent.h" +#include "CreateEventContent.h" +#include "HistoryVisibilityEventContent.h" +#include "JoinRulesEventContent.h" +#include "NameEventContent.h" +#include "PowerLevelsEventContent.h" +#include "TopicEventContent.h" + +#include "StateEvent.h" + +namespace events = matrix::events; + ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent) : QWidget(parent) , ui(new Ui::ChatPage) @@ -55,16 +69,9 @@ ChatPage::ChatPage(QSharedPointer<MatrixClient> client, QWidget *parent) connect(user_info_widget_, SIGNAL(logout()), client_.data(), SLOT(logout())); connect(client_.data(), SIGNAL(loggedOut()), this, SLOT(logout())); - connect(room_list_, - SIGNAL(roomChanged(const RoomInfo &)), - this, - SLOT(changeTopRoomInfo(const RoomInfo &))); - connect(room_list_, - SIGNAL(roomChanged(const RoomInfo &)), - view_manager_, - SLOT(setHistoryView(const RoomInfo &))); + connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo); + connect(room_list_, &RoomList::roomChanged, view_manager_, &TimelineViewManager::setHistoryView); - // TODO: Better pass the whole RoomInfo struct instead of the roomid. connect(view_manager_, SIGNAL(unreadMessages(const QString &, int)), room_list_, @@ -161,7 +168,24 @@ void ChatPage::syncCompleted(const SyncResponse &response) { client_->setNextBatchToken(response.nextBatch()); - /* room_list_->sync(response.rooms()); */ + auto joined = response.rooms().join(); + + for (auto it = joined.constBegin(); it != joined.constEnd(); it++) { + RoomState room_state; + + if (state_manager_.contains(it.key())) + room_state = state_manager_[it.key()]; + + updateRoomState(room_state, it.value().state().events()); + updateRoomState(room_state, it.value().timeline().events()); + + state_manager_.insert(it.key(), room_state); + + if (it.key() == current_room_) + changeTopRoomInfo(it.key()); + } + + room_list_->sync(state_manager_); view_manager_->sync(response.rooms()); sync_timer_->start(sync_interval_); @@ -172,8 +196,19 @@ void ChatPage::initialSyncCompleted(const SyncResponse &response) if (!response.nextBatch().isEmpty()) client_->setNextBatchToken(response.nextBatch()); + auto joined = response.rooms().join(); + + for (auto it = joined.constBegin(); it != joined.constEnd(); it++) { + RoomState room_state; + + updateRoomState(room_state, it.value().state().events()); + updateRoomState(room_state, it.value().timeline().events()); + + state_manager_.insert(it.key(), room_state); + } + view_manager_->initialize(response.rooms()); - room_list_->setInitialRooms(response.rooms()); + room_list_->setInitialRooms(state_manager_); sync_timer_->start(sync_interval_); } @@ -182,7 +217,7 @@ void ChatPage::updateTopBarAvatar(const QString &roomid, const QPixmap &img) { room_avatars_.insert(roomid, img); - if (current_room_.id() != roomid) + if (current_room_ != roomid) return; top_bar_->updateRoomAvatar(img.toImage()); @@ -199,17 +234,22 @@ void ChatPage::updateOwnProfileInfo(const QUrl &avatar_url, const QString &displ client_->fetchOwnAvatar(avatar_url); } -void ChatPage::changeTopRoomInfo(const RoomInfo &info) +void ChatPage::changeTopRoomInfo(const QString &room_id) { - top_bar_->updateRoomName(info.name()); - top_bar_->updateRoomTopic(info.topic()); + if (!state_manager_.contains(room_id)) + return; + + auto state = state_manager_[room_id]; + + top_bar_->updateRoomName(state.resolveName()); + top_bar_->updateRoomTopic(state.resolveTopic()); - if (room_avatars_.contains(info.id())) - top_bar_->updateRoomAvatar(room_avatars_.value(info.id()).toImage()); + if (room_avatars_.contains(room_id)) + top_bar_->updateRoomAvatar(room_avatars_.value(room_id).toImage()); else - top_bar_->updateRoomAvatarFromName(info.name()); + top_bar_->updateRoomAvatarFromName(state.resolveName()); - current_room_ = info; + current_room_ = room_id; } void ChatPage::showUnreadMessageNotification(int count) @@ -221,6 +261,88 @@ void ChatPage::showUnreadMessageNotification(int count) emit changeWindowTitle(QString("nheko (%1)").arg(count)); } +void ChatPage::updateRoomState(RoomState &room_state, const QJsonArray &events) +{ + events::EventType ty; + + for (const auto &event : events) { + try { + ty = events::extractEventType(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } + + if (!events::isStateEvent(ty)) + continue; + + try { + switch (ty) { + case events::EventType::RoomAliases: { + events::StateEvent<events::AliasesEventContent> aliases; + aliases.deserialize(event); + room_state.aliases = aliases; + break; + } + case events::EventType::RoomAvatar: { + events::StateEvent<events::AvatarEventContent> avatar; + avatar.deserialize(event); + room_state.avatar = avatar; + break; + } + case events::EventType::RoomCanonicalAlias: { + events::StateEvent<events::CanonicalAliasEventContent> canonical_alias; + canonical_alias.deserialize(event); + room_state.canonical_alias = canonical_alias; + break; + } + case events::EventType::RoomCreate: { + events::StateEvent<events::CreateEventContent> create; + create.deserialize(event); + room_state.create = create; + break; + } + case events::EventType::RoomHistoryVisibility: { + events::StateEvent<events::HistoryVisibilityEventContent> history_visibility; + history_visibility.deserialize(event); + room_state.history_visibility = history_visibility; + break; + } + case events::EventType::RoomJoinRules: { + events::StateEvent<events::JoinRulesEventContent> join_rules; + join_rules.deserialize(event); + room_state.join_rules = join_rules; + break; + } + case events::EventType::RoomName: { + events::StateEvent<events::NameEventContent> name; + name.deserialize(event); + room_state.name = name; + break; + } + case events::EventType::RoomPowerLevels: { + events::StateEvent<events::PowerLevelsEventContent> power_levels; + power_levels.deserialize(event); + room_state.power_levels = power_levels; + break; + } + case events::EventType::RoomTopic: { + events::StateEvent<events::TopicEventContent> topic; + topic.deserialize(event); + room_state.topic = topic; + break; + } + default: { + continue; + } + } + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } + } +} + ChatPage::~ChatPage() { sync_timer_->stop(); diff --git a/src/ImageItem.cc b/src/ImageItem.cc
index d03e41b5..e0e2f977 100644 --- a/src/ImageItem.cc +++ b/src/ImageItem.cc
@@ -25,10 +25,11 @@ #include "ImageItem.h" #include "ImageOverlayDialog.h" -ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const Event &event, const QString &body, const QUrl &url, QWidget *parent) +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + +ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const events::MessageEvent<msgs::Image> &event, QWidget *parent) : QWidget(parent) - , url_{url} - , text_{body} , event_{event} , client_{client} { @@ -37,6 +38,9 @@ ImageItem::ImageItem(QSharedPointer<MatrixClient> client, const Event &event, co setCursor(Qt::PointingHandCursor); setAttribute(Qt::WA_Hover, true); + url_ = event.msgContent().url(); + text_ = event.content().body(); + QList<QString> url_parts = url_.toString().split("mxc://"); if (url_parts.size() != 2) { diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc
index 6b4a81bb..f9d81f27 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc
@@ -194,10 +194,12 @@ void MatrixClient::onInitialSyncResponse(QNetworkReply *reply) try { response.deserialize(json); - emit initialSyncCompleted(response); } catch (DeserializationException &e) { qWarning() << "Sync malformed response" << e.what(); + return; } + + emit initialSyncCompleted(response); } void MatrixClient::onSyncResponse(QNetworkReply *reply) diff --git a/src/RoomInfoListItem.cc b/src/RoomInfoListItem.cc
index 954025c6..6e632a6a 100644 --- a/src/RoomInfoListItem.cc +++ b/src/RoomInfoListItem.cc
@@ -19,12 +19,13 @@ #include <QMouseEvent> #include "Ripple.h" -#include "RoomInfo.h" #include "RoomInfoListItem.h" +#include "RoomState.h" -RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) +RoomInfoListItem::RoomInfoListItem(RoomState state, QString room_id, QWidget *parent) : QWidget(parent) - , info_(info) + , state_(state) + , room_id_(room_id) , is_pressed_(false) , max_height_(60) , unread_msg_count_(0) @@ -43,6 +44,9 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) setMaximumSize(parent->width(), max_height_); + QString room_name = state_.resolveName(); + QString room_topic = state_.topic.content().topic().simplified(); + topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); topLayout_->setMargin(0); @@ -60,7 +64,7 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) textLayout_->setContentsMargins(0, 5, 0, 5); roomAvatar_ = new Avatar(avatarWidget_); - roomAvatar_->setLetter(QChar(info_.name()[0])); + roomAvatar_->setLetter(QChar(room_name[0])); roomAvatar_->setSize(max_height_ - 20); roomAvatar_->setTextColor("#555459"); roomAvatar_->setBackgroundColor("#d6dde3"); @@ -76,12 +80,12 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) avatarLayout_->addWidget(roomAvatar_); - roomName_ = new QLabel(info_.name(), textWidget_); + roomName_ = new QLabel(room_name, textWidget_); roomName_->setMaximumSize(parent->width() - max_height_, 20); roomName_->setFont(QFont("Open Sans", 11)); roomName_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - roomTopic_ = new QLabel(info_.topic(), textWidget_); + roomTopic_ = new QLabel(room_topic, textWidget_); roomTopic_->setMaximumSize(parent->width() - max_height_, 20); roomTopic_->setFont(QFont("Open Sans", 10)); roomTopic_->setStyleSheet("color: #171919"); @@ -93,8 +97,8 @@ RoomInfoListItem::RoomInfoListItem(RoomInfo info, QWidget *parent) topLayout_->addWidget(avatarWidget_); topLayout_->addWidget(textWidget_); - setElidedText(roomName_, info_.name(), parent->width() - max_height_); - setElidedText(roomTopic_, info_.topic(), parent->width() - max_height_); + setElidedText(roomName_, room_name, parent->width() - max_height_); + setElidedText(roomTopic_, room_topic, parent->width() - max_height_); QPainterPath path; path.addRoundedRect(rect(), 0, 0); @@ -131,9 +135,23 @@ void RoomInfoListItem::setPressedState(bool state) } } +void RoomInfoListItem::setState(const RoomState &new_state) +{ + if (state_.resolveName() != new_state.resolveName()) + setElidedText(roomName_, new_state.resolveName(), parentWidget()->width() - max_height_); + + if (state_.resolveTopic() != new_state.resolveTopic()) + setElidedText(roomTopic_, new_state.resolveTopic(), parentWidget()->width() - max_height_); + + if (new_state.avatar.content().url().toString().isEmpty()) + roomAvatar_->setLetter(QChar(new_state.resolveName()[0])); + + state_ = new_state; +} + void RoomInfoListItem::mousePressEvent(QMouseEvent *event) { - emit clicked(info_); + emit clicked(room_id_); setPressedState(true); diff --git a/src/RoomList.cc b/src/RoomList.cc
index 4fbccee0..a0312113 100644 --- a/src/RoomList.cc +++ b/src/RoomList.cc
@@ -57,32 +57,6 @@ void RoomList::clear() rooms_.clear(); } -RoomInfo RoomList::extractRoomInfo(const State &room_state) -{ - RoomInfo info; - - auto events = room_state.events(); - - for (const auto &event : events) { - if (event.type() == "m.room.name") { - info.setName(event.content().value("name").toString()); - } else if (event.type() == "m.room.topic") { - info.setTopic(event.content().value("topic").toString()); - } else if (event.type() == "m.room.avatar") { - info.setAvatarUrl(QUrl(event.content().value("url").toString())); - } else if (event.type() == "m.room.canonical_alias") { - if (info.name().isEmpty()) - info.setName(event.content().value("alias").toString()); - } - } - - // Sanitize info for print. - info.setTopic(info.topic().simplified()); - info.setName(info.name().simplified()); - - return info; -} - void RoomList::updateUnreadMessageCount(const QString &roomid, int count) { if (!rooms_.contains(roomid)) { @@ -105,27 +79,21 @@ void RoomList::calculateUnreadMessageCount() emit totalUnreadMessageCountUpdated(total_unread_msgs); } -void RoomList::setInitialRooms(const Rooms &rooms) +void RoomList::setInitialRooms(const QMap<QString, RoomState> &states) { rooms_.clear(); - for (auto it = rooms.join().constBegin(); it != rooms.join().constEnd(); it++) { - RoomInfo info = RoomList::extractRoomInfo(it.value().state()); - info.setId(it.key()); + 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::sync(const QMap<QString, RoomState> &states) +{ + for (auto it = states.constBegin(); it != states.constEnd(); it++) { + auto room_id = it.key(); + auto state = it.value(); + + // TODO: Add the new room to the list. + if (!rooms_.contains(room_id)) + continue; + + auto room = rooms_[room_id]; + + auto current_avatar = room->state().avatar.content().url(); + auto new_avatar = state.avatar.content().url(); + + if (current_avatar != new_avatar && !new_avatar.toString().isEmpty()) + client_->fetchRoomAvatar(room_id, new_avatar); + + room->setState(state); + } } -void RoomList::highlightSelectedRoom(const RoomInfo &info) +void RoomList::highlightSelectedRoom(const QString &room_id) { - emit roomChanged(info); + emit roomChanged(room_id); - if (!rooms_.contains(info.id())) { + if (!rooms_.contains(room_id)) { qDebug() << "RoomList: clicked unknown roomid"; return; } // TODO: Send a read receipt for the last event. - auto room = rooms_[info.id()]; + auto room = rooms_[room_id]; room->clearUnreadMessageCount(); calculateUnreadMessageCount(); for (auto it = rooms_.constBegin(); it != rooms_.constEnd(); it++) { - if (it.key() != info.id()) + if (it.key() != room_id) it.value()->setPressedState(false); } } diff --git a/src/RoomState.cc b/src/RoomState.cc new file mode 100644
index 00000000..98f418e3 --- /dev/null +++ b/src/RoomState.cc
@@ -0,0 +1,32 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "RoomState.h" + +QString RoomState::resolveName() const +{ + if (!name.content().name().isEmpty()) + return name.content().name().simplified(); + + if (!canonical_alias.content().alias().isEmpty()) + return canonical_alias.content().alias().simplified(); + + if (aliases.content().aliases().size() != 0) + return aliases.content().aliases()[0].simplified(); + + return "Unknown Room Name"; +} diff --git a/src/Sync.cc b/src/Sync.cc
index 50b49fc6..0d04e878 100644 --- a/src/Sync.cc +++ b/src/Sync.cc
@@ -157,19 +157,7 @@ void State::deserialize(const QJsonValue &data) if (!data.isArray()) throw DeserializationException("State is not a JSON array"); - QJsonArray event_array = data.toArray(); - - for (int i = 0; i < event_array.count(); i++) { - Event event; - - try { - event.deserialize(event_array.at(i)); - events_.push_back(event); - } catch (DeserializationException &e) { - qWarning() << e.what(); - qWarning() << "Skipping malformed state event"; - } - } + events_ = data.toArray(); } void Timeline::deserialize(const QJsonValue &data) @@ -194,17 +182,5 @@ void Timeline::deserialize(const QJsonValue &data) if (!object.value("events").isArray()) throw DeserializationException("timeline/events is not a JSON array"); - auto timeline_events = object.value("events").toArray(); - - for (int i = 0; i < timeline_events.count(); i++) { - Event event; - - try { - event.deserialize(timeline_events.at(i)); - events_.push_back(event); - } catch (DeserializationException &e) { - qWarning() << e.what(); - qWarning() << "Skipping malformed timeline event"; - } - } + events_ = object.value("events").toArray(); } diff --git a/src/TimelineItem.cc b/src/TimelineItem.cc
index 4d33db70..8d5e503a 100644 --- a/src/TimelineItem.cc +++ b/src/TimelineItem.cc
@@ -21,6 +21,9 @@ #include "ImageItem.h" #include "TimelineItem.h" +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + TimelineItem::TimelineItem(const QString &userid, const QString &color, const QString &body, QWidget *parent) : QWidget(parent) { @@ -37,7 +40,7 @@ TimelineItem::TimelineItem(const QString &body, QWidget *parent) setupLayout(); } -TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString &color, QWidget *parent) +TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, const QString &color, QWidget *parent) : QWidget(parent) { auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); @@ -58,7 +61,7 @@ TimelineItem::TimelineItem(ImageItem *image, const Event &event, const QString & setLayout(top_layout_); } -TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent) +TimelineItem::TimelineItem(ImageItem *image, const events::MessageEvent<msgs::Image> &event, QWidget *parent) : QWidget(parent) { auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); @@ -73,16 +76,31 @@ TimelineItem::TimelineItem(ImageItem *image, const Event &event, QWidget *parent setLayout(top_layout_); } -TimelineItem::TimelineItem(const Event &event, bool with_sender, const QString &color, QWidget *parent) +TimelineItem::TimelineItem(const events::MessageEvent<msgs::Notice> &event, bool with_sender, const QString &color, QWidget *parent) : QWidget(parent) { - auto body = event.content().value("body").toString().trimmed().toHtmlEscaped(); + auto body = event.content().body().trimmed().toHtmlEscaped(); auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); generateTimestamp(timestamp); - if (event.content().value("msgtype").toString() == "m.notice") - body = "<i style=\"color: #565E5E\">" + body + "</i>"; + body = "<i style=\"color: #565E5E\">" + body + "</i>"; + + if (with_sender) + generateBody(event.sender(), color, body); + else + generateBody(body); + + setupLayout(); +} + +TimelineItem::TimelineItem(const events::MessageEvent<msgs::Text> &event, bool with_sender, const QString &color, QWidget *parent) + : QWidget(parent) +{ + auto body = event.content().body().trimmed().toHtmlEscaped(); + auto timestamp = QDateTime::fromMSecsSinceEpoch(event.timestamp()); + + generateTimestamp(timestamp); if (with_sender) generateBody(event.sender(), color, body); diff --git a/src/TimelineView.cc b/src/TimelineView.cc
index 95c7a351..686fd602 100644 --- a/src/TimelineView.cc +++ b/src/TimelineView.cc
@@ -16,17 +16,25 @@ */ #include <QDebug> +#include <QJsonArray> #include <QScrollBar> #include <QSettings> #include <QtWidgets/QLabel> #include <QtWidgets/QSpacerItem> +#include "Event.h" +#include "MessageEvent.h" +#include "MessageEventContent.h" + #include "ImageItem.h" #include "TimelineItem.h" #include "TimelineView.h" #include "TimelineViewManager.h" -TimelineView::TimelineView(const QList<Event> &events, QSharedPointer<MatrixClient> client, QWidget *parent) +namespace events = matrix::events; +namespace msgs = matrix::events::messages; + +TimelineView::TimelineView(const QJsonArray &events, QSharedPointer<MatrixClient> client, QWidget *parent) : QWidget(parent) , client_{client} { @@ -53,52 +61,79 @@ void TimelineView::sliderRangeChanged(int min, int max) scroll_area_->verticalScrollBar()->setValue(max); } -int TimelineView::addEvents(const QList<Event> &events) +int TimelineView::addEvents(const QJsonArray &events) { QSettings settings; auto local_user = settings.value("auth/user_id").toString(); int message_count = 0; + events::EventType ty; for (const auto &event : events) { - if (event.type() == "m.room.message") { - auto msg_type = event.content().value("msgtype").toString(); + ty = events::extractEventType(event.toObject()); - if (isPendingMessage(event, local_user)) { - removePendingMessage(event); - continue; - } + if (ty == events::RoomMessage) { + events::MessageEventType msg_type = events::extractMessageEventType(event.toObject()); - if (msg_type == "m.text" || msg_type == "m.notice") { - auto with_sender = last_sender_ != event.sender(); - auto color = TimelineViewManager::getUserColor(event.sender()); + if (msg_type == events::MessageEventType::Text) { + events::MessageEvent<msgs::Text> text; - addHistoryItem(event, color, with_sender); - last_sender_ = event.sender(); + try { + text.deserialize(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } - message_count += 1; - } else if (msg_type == "m.image") { - // TODO: Move this into serialization. - if (!event.content().contains("url")) { - qWarning() << "Missing url from m.image event" << event.content(); + if (isPendingMessage(text, local_user)) { + removePendingMessage(text); continue; } - if (!event.content().contains("body")) { - qWarning() << "Missing body from m.image event" << event.content(); + auto with_sender = last_sender_ != text.sender(); + auto color = TimelineViewManager::getUserColor(text.sender()); + + addHistoryItem(text, color, with_sender); + last_sender_ = text.sender(); + + message_count += 1; + } else if (msg_type == events::MessageEventType::Notice) { + events::MessageEvent<msgs::Notice> notice; + + try { + notice.deserialize(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; continue; } - QUrl url(event.content().value("url").toString()); - QString body(event.content().value("body").toString()); + auto with_sender = last_sender_ != notice.sender(); + auto color = TimelineViewManager::getUserColor(notice.sender()); - auto with_sender = last_sender_ != event.sender(); - auto color = TimelineViewManager::getUserColor(event.sender()); + addHistoryItem(notice, color, with_sender); + last_sender_ = notice.sender(); - addImageItem(body, url, event, color, with_sender); + message_count += 1; + } else if (msg_type == events::MessageEventType::Image) { + events::MessageEvent<msgs::Image> img; - last_sender_ = event.sender(); + try { + img.deserialize(event.toObject()); + } catch (const DeserializationException &e) { + qWarning() << e.what() << event; + continue; + } + + auto with_sender = last_sender_ != img.sender(); + auto color = TimelineViewManager::getUserColor(img.sender()); + + addHistoryItem(img, color, with_sender); + + last_sender_ = img.sender(); message_count += 1; + } else if (msg_type == events::MessageEventType::Unknown) { + qWarning() << "Unknown message type" << event.toObject(); + continue; } } } @@ -136,13 +171,9 @@ void TimelineView::init() SLOT(sliderRangeChanged(int, int))); } -void TimelineView::addImageItem(const QString &body, - const QUrl &url, - const Event &event, - const QString &color, - bool with_sender) +void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Image> &event, const QString &color, bool with_sender) { - auto image = new ImageItem(client_, event, body, url); + auto image = new ImageItem(client_, event); if (with_sender) { auto item = new TimelineItem(image, event, color, scroll_widget_); @@ -153,7 +184,13 @@ void TimelineView::addImageItem(const QString &body, } } -void TimelineView::addHistoryItem(const Event &event, const QString &color, bool with_sender) +void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Notice> &event, const QString &color, bool with_sender) +{ + TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_); + scroll_layout_->addWidget(item); +} + +void TimelineView::addHistoryItem(const events::MessageEvent<msgs::Text> &event, const QString &color, bool with_sender) { TimelineItem *item = new TimelineItem(event, with_sender, color, scroll_widget_); scroll_layout_->addWidget(item); @@ -169,34 +206,25 @@ void TimelineView::updatePendingMessage(int txn_id, QString event_id) } } -bool TimelineView::isPendingMessage(const Event &event, const QString &userid) +bool TimelineView::isPendingMessage(const events::MessageEvent<msgs::Text> &e, const QString &local_userid) { - if (event.sender() != userid || event.type() != "m.room.message") - return false; - - auto msgtype = event.content().value("msgtype").toString(); - auto body = event.content().value("body").toString(); - - // FIXME: should contain more checks later on for other types of messages. - if (msgtype != "m.text") + if (e.sender() != local_userid) return false; for (const auto &msg : pending_msgs_) { - if (msg.event_id == event.eventId() || msg.body == body) + if (msg.event_id == e.eventId() || msg.body == e.content().body()) return true; } return false; } -void TimelineView::removePendingMessage(const Event &event) +void TimelineView::removePendingMessage(const events::MessageEvent<msgs::Text> &e) { - auto body = event.content().value("body").toString(); - for (auto it = pending_msgs_.begin(); it != pending_msgs_.end(); it++) { int index = std::distance(pending_msgs_.begin(), it); - if (it->event_id == event.eventId() || it->body == body) { + if (it->event_id == e.eventId() || it->body == e.content().body()) { pending_msgs_.removeAt(index); break; } diff --git a/src/TimelineViewManager.cc b/src/TimelineViewManager.cc
index ddb142d3..bf3dd997 100644 --- a/src/TimelineViewManager.cc +++ b/src/TimelineViewManager.cc
@@ -54,11 +54,11 @@ void TimelineViewManager::messageSent(const QString &event_id, const QString &ro void TimelineViewManager::sendTextMessage(const QString &msg) { - auto room = active_room_; - auto view = views_[room.id()]; + auto room_id = active_room_; + auto view = views_[room_id]; view->addUserTextMessage(msg, client_->transactionId()); - client_->sendTextMessage(room.id(), msg); + client_->sendTextMessage(room_id, msg); } void TimelineViewManager::clearAll() @@ -95,7 +95,7 @@ void TimelineViewManager::sync(const Rooms &rooms) auto roomid = it.key(); if (!views_.contains(roomid)) { - qDebug() << "Ignoring event from unknown room"; + qDebug() << "Ignoring event from unknown room" << roomid; continue; } @@ -105,26 +105,25 @@ void TimelineViewManager::sync(const Rooms &rooms) int msgs_added = view->addEvents(events); if (msgs_added > 0) { - // TODO: When window gets active the current + // TODO: When the app window gets active the current // unread count (if any) should be cleared. auto isAppActive = QApplication::activeWindow() != nullptr; - if (roomid != active_room_.id() || !isAppActive) + if (roomid != active_room_ || !isAppActive) emit unreadMessages(roomid, msgs_added); } } } -void TimelineViewManager::setHistoryView(const RoomInfo &info) +void TimelineViewManager::setHistoryView(const QString &room_id) { - if (!views_.contains(info.id())) { - qDebug() << "Room List id is not present in view manager"; - qDebug() << info.name(); + if (!views_.contains(room_id)) { + qDebug() << "Room ID from RoomList is not present in ViewManager" << room_id; return; } - active_room_ = info; - auto widget = views_.value(info.id()); + active_room_ = room_id; + auto widget = views_.value(room_id); setCurrentWidget(widget); } diff --git a/src/events/Event.cc b/src/events/Event.cc
index 9a8590e0..da4f3e99 100644 --- a/src/events/Event.cc +++ b/src/events/Event.cc
@@ -50,6 +50,8 @@ matrix::events::EventType matrix::events::extractEventType(const QJsonObject &ob return EventType::RoomJoinRules; else if (type == "m.room.member") return EventType::RoomMember; + else if (type == "m.room.message") + return EventType::RoomMessage; else if (type == "m.room.name") return EventType::RoomName; else if (type == "m.room.power_levels") @@ -59,3 +61,22 @@ matrix::events::EventType matrix::events::extractEventType(const QJsonObject &ob else return EventType::Unsupported; } + +bool matrix::events::isStateEvent(EventType type) +{ + return type == EventType::RoomAliases || + type == EventType::RoomAvatar || + type == EventType::RoomCanonicalAlias || + type == EventType::RoomCreate || + type == EventType::RoomHistoryVisibility || + type == EventType::RoomJoinRules || + type == EventType::RoomMember || + type == EventType::RoomName || + type == EventType::RoomPowerLevels || + type == EventType::RoomTopic; +} + +bool matrix::events::isMessageEvent(EventType type) +{ + return type == EventType::RoomMessage; +} diff --git a/src/events/MessageEventContent.cc b/src/events/MessageEventContent.cc new file mode 100644
index 00000000..df2c39e8 --- /dev/null +++ b/src/events/MessageEventContent.cc
@@ -0,0 +1,63 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <QDebug> + +#include "MessageEventContent.h" + +using namespace matrix::events; + +MessageEventType matrix::events::extractMessageEventType(const QJsonObject &data) +{ + if (!data.contains("content")) + return MessageEventType::Unknown; + + auto content = data.value("content").toObject(); + auto msgtype = content.value("msgtype").toString(); + + if (msgtype == "m.audio") + return MessageEventType::Audio; + else if (msgtype == "m.emote") + return MessageEventType::Emote; + else if (msgtype == "m.file") + return MessageEventType::File; + else if (msgtype == "m.image") + return MessageEventType::Image; + else if (msgtype == "m.location") + return MessageEventType::Location; + else if (msgtype == "m.notice") + return MessageEventType::Notice; + else if (msgtype == "m.text") + return MessageEventType::Text; + else if (msgtype == "m.video") + return MessageEventType::Video; + else + return MessageEventType::Unknown; +} + +void MessageEventContent::deserialize(const QJsonValue &data) +{ + if (!data.isObject()) + throw DeserializationException("MessageEventContent is not a JSON object"); + + auto object = data.toObject(); + + if (!object.contains("body")) + throw DeserializationException("body key is missing"); + + body_ = object.value("body").toString(); +} diff --git a/src/RoomInfo.cc b/src/events/messages/Audio.cc
index f8a7c56a..f0fb443b 100644 --- a/src/RoomInfo.cc +++ b/src/events/messages/Audio.cc
@@ -15,57 +15,25 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include "RoomInfo.h" +#include "Audio.h" -RoomInfo::RoomInfo() - : name_("") - , topic_("") -{ -} - -RoomInfo::RoomInfo(QString name, QString topic, QUrl avatar_url) - : name_(name) - , topic_(topic) - , avatar_url_(avatar_url) -{ -} - -QString RoomInfo::id() const -{ - return id_; -} - -QString RoomInfo::name() const -{ - return name_; -} +using namespace matrix::events::messages; -QString RoomInfo::topic() const +void Audio::deserialize(const QJsonObject &object) { - return topic_; -} + if (!object.contains("url")) + throw DeserializationException("url key is missing"); -QUrl RoomInfo::avatarUrl() const -{ - return avatar_url_; -} + url_ = object.value("url").toString(); -void RoomInfo::setAvatarUrl(const QUrl &url) -{ - avatar_url_ = url; -} + if (object.value("msgtype") != "m.audio") + throw DeserializationException("invalid msgtype for audio"); -void RoomInfo::setId(const QString &id) -{ - id_ = id; -} + if (object.contains("info")) { + auto info = object.value("info").toObject(); -void RoomInfo::setName(const QString &name) -{ - name_ = name; -} - -void RoomInfo::setTopic(const QString &topic) -{ - topic_ = topic; + info_.duration = info.value("duration").toInt(); + info_.mimetype = info.value("mimetype").toString(); + info_.size = info.value("size").toInt(); + } } diff --git a/src/events/messages/Emote.cc b/src/events/messages/Emote.cc new file mode 100644
index 00000000..1d6a4753 --- /dev/null +++ b/src/events/messages/Emote.cc
@@ -0,0 +1,26 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Emote.h" + +using namespace matrix::events::messages; + +void Emote::deserialize(const QJsonObject &object) +{ + if (object.value("msgtype") != "m.emote") + throw DeserializationException("invalid msgtype for emote"); +} diff --git a/src/events/messages/File.cc b/src/events/messages/File.cc new file mode 100644
index 00000000..a6b5b6c2 --- /dev/null +++ b/src/events/messages/File.cc
@@ -0,0 +1,51 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "File.h" + +using namespace matrix::events::messages; + +void File::deserialize(const QJsonObject &object) +{ + if (!object.contains("url")) + throw DeserializationException("messages::File url key is missing"); + + if (!object.contains("filename")) + throw DeserializationException("messages::File filename key is missing"); + + if (object.value("msgtype") != "m.file") + throw DeserializationException("invalid msgtype for file"); + + url_ = object.value("url").toString(); + + if (object.contains("info")) { + auto file_info = object.value("info").toObject(); + + info_.size = file_info.value("size").toInt(); + info_.mimetype = file_info.value("mimetype").toString(); + info_.thumbnail_url = file_info.value("thumbnail_url").toString(); + + if (file_info.contains("thumbnail_info")) { + auto thumbinfo = file_info.value("thumbnail_info").toObject(); + + info_.thumbnail_info.h = thumbinfo.value("h").toInt(); + info_.thumbnail_info.w = thumbinfo.value("w").toInt(); + info_.thumbnail_info.size = thumbinfo.value("size").toInt(); + info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString(); + } + } +} diff --git a/src/events/messages/Image.cc b/src/events/messages/Image.cc new file mode 100644
index 00000000..d528e174 --- /dev/null +++ b/src/events/messages/Image.cc
@@ -0,0 +1,51 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Image.h" + +using namespace matrix::events::messages; + +void Image::deserialize(const QJsonObject &object) +{ + if (!object.contains("url")) + throw DeserializationException("messages::Image url key is missing"); + + url_ = object.value("url").toString(); + + if (object.value("msgtype") != "m.image") + throw DeserializationException("invalid msgtype for image"); + + if (object.contains("info")) { + auto imginfo = object.value("info").toObject(); + + info_.w = imginfo.value("w").toInt(); + info_.h = imginfo.value("h").toInt(); + info_.size = imginfo.value("size").toInt(); + + info_.mimetype = imginfo.value("mimetype").toString(); + info_.thumbnail_url = imginfo.value("thumbnail_url").toString(); + + if (imginfo.contains("thumbnail_info")) { + auto thumbinfo = imginfo.value("thumbnail_info").toObject(); + + info_.thumbnail_info.h = thumbinfo.value("h").toInt(); + info_.thumbnail_info.w = thumbinfo.value("w").toInt(); + info_.thumbnail_info.size = thumbinfo.value("size").toInt(); + info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString(); + } + } +} diff --git a/src/events/messages/Location.cc b/src/events/messages/Location.cc new file mode 100644
index 00000000..68a9a9c1 --- /dev/null +++ b/src/events/messages/Location.cc
@@ -0,0 +1,46 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Location.h" + +using namespace matrix::events::messages; + +void Location::deserialize(const QJsonObject &object) +{ + if (!object.contains("geo_uri")) + throw DeserializationException("messages::Location geo_uri key is missing"); + + if (object.value("msgtype") != "m.location") + throw DeserializationException("invalid msgtype for location"); + + geo_uri_ = object.value("geo_uri").toString(); + + if (object.contains("info")) { + auto location_info = object.value("info").toObject(); + + info_.thumbnail_url = location_info.value("thumbnail_url").toString(); + + if (location_info.contains("thumbnail_info")) { + auto thumbinfo = location_info.value("thumbnail_info").toObject(); + + info_.thumbnail_info.h = thumbinfo.value("h").toInt(); + info_.thumbnail_info.w = thumbinfo.value("w").toInt(); + info_.thumbnail_info.size = thumbinfo.value("size").toInt(); + info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString(); + } + } +} diff --git a/src/events/messages/Notice.cc b/src/events/messages/Notice.cc new file mode 100644
index 00000000..1dd4cc28 --- /dev/null +++ b/src/events/messages/Notice.cc
@@ -0,0 +1,26 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Notice.h" + +using namespace matrix::events::messages; + +void Notice::deserialize(const QJsonObject &object) +{ + if (object.value("msgtype") != "m.notice") + throw DeserializationException("invalid msgtype for notice"); +} diff --git a/src/events/messages/Text.cc b/src/events/messages/Text.cc new file mode 100644
index 00000000..5446d7f4 --- /dev/null +++ b/src/events/messages/Text.cc
@@ -0,0 +1,26 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Text.h" + +using namespace matrix::events::messages; + +void Text::deserialize(const QJsonObject &object) +{ + if (object.value("msgtype") != "m.text") + throw DeserializationException("invalid msgtype for text"); +} diff --git a/src/events/messages/Video.cc b/src/events/messages/Video.cc new file mode 100644
index 00000000..a7ddba96 --- /dev/null +++ b/src/events/messages/Video.cc
@@ -0,0 +1,52 @@ +/* + * nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Video.h" + +using namespace matrix::events::messages; + +void Video::deserialize(const QJsonObject &object) +{ + if (!object.contains("url")) + throw DeserializationException("messages::Video url key is missing"); + + url_ = object.value("url").toString(); + + if (object.value("msgtype") != "m.video") + throw DeserializationException("invalid msgtype for video"); + + if (object.contains("info")) { + auto video_info = object.value("info").toObject(); + + info_.w = video_info.value("w").toInt(); + info_.h = video_info.value("h").toInt(); + info_.size = video_info.value("size").toInt(); + info_.duration = video_info.value("duration").toInt(); + + info_.mimetype = video_info.value("mimetype").toString(); + info_.thumbnail_url = video_info.value("thumbnail_url").toString(); + + if (video_info.contains("thumbnail_info")) { + auto thumbinfo = video_info.value("thumbnail_info").toObject(); + + info_.thumbnail_info.h = thumbinfo.value("h").toInt(); + info_.thumbnail_info.w = thumbinfo.value("w").toInt(); + info_.thumbnail_info.size = thumbinfo.value("size").toInt(); + info_.thumbnail_info.mimetype = thumbinfo.value("mimetype").toString(); + } + } +}