From 91d1f19058a31cc35ca1212f042a9dd6f501a7b7 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:06:10 +0100 Subject: Remove old timeline --- src/timeline/TimelineModel.cpp | 1220 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1220 insertions(+) create mode 100644 src/timeline/TimelineModel.cpp (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp new file mode 100644 index 00000000..ab7d3d47 --- /dev/null +++ b/src/timeline/TimelineModel.cpp @@ -0,0 +1,1220 @@ +#include "TimelineModel.h" + +#include +#include + +#include + +#include "ChatPage.h" +#include "Logging.h" +#include "MainWindow.h" +#include "Olm.h" +#include "TimelineViewManager.h" +#include "Utils.h" +#include "dialogs/RawMessage.h" + +Q_DECLARE_METATYPE(QModelIndex) + +namespace { +template +QString +eventId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.event_id); +} +template +QString +roomId(const mtx::events::Event &event) +{ + return QString::fromStdString(event.room_id); +} +template +QString +senderId(const mtx::events::RoomEvent &event) +{ + return QString::fromStdString(event.sender); +} + +template +QDateTime +eventTimestamp(const mtx::events::RoomEvent &event) +{ + return QDateTime::fromMSecsSinceEpoch(event.origin_server_ts); +} + +template +std::string +eventMsgType(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype) +{ + return e.content.msgtype; +} + +template +QString +eventBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.body); +} + +template +QString +eventFormattedBody(const mtx::events::Event &) +{ + return QString(""); +} +template +auto +eventFormattedBody(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + auto temp = e.content.formatted_body; + if (!temp.empty()) { + return QString::fromStdString(temp); + } else { + return QString::fromStdString(e.content.body).toHtmlEscaped().replace("\n", "
"); + } +} + +template +QString +eventUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.url); +} + +template +QString +eventThumbnailUrl(const mtx::events::Event &) +{ + return ""; +} +template +auto +eventThumbnailUrl(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, + QString> +{ + return QString::fromStdString(e.content.info.thumbnail_url); +} + +template +QString +eventFilename(const mtx::events::Event &) +{ + return ""; +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + return QString::fromStdString(e.content.body); +} +QString +eventFilename(const mtx::events::RoomEvent &e) +{ + // body may be the original filename + if (!e.content.filename.empty()) + return QString::fromStdString(e.content.filename); + return QString::fromStdString(e.content.body); +} + +template +auto +eventFilesize(const mtx::events::RoomEvent &e) -> decltype(e.content.info.size) +{ + return e.content.info.size; +} + +template +int64_t +eventFilesize(const mtx::events::Event &) +{ + return 0; +} + +template +QString +eventMimeType(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventMimeType(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, QString> +{ + return QString::fromStdString(e.content.info.mimetype); +} + +template +QString +eventRelatesTo(const mtx::events::Event &) +{ + return QString(); +} +template +auto +eventRelatesTo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same::value, + QString> +{ + return QString::fromStdString(e.content.relates_to.in_reply_to.event_id); +} + +template +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &e) +{ + using mtx::events::EventType; + switch (e.type) { + case EventType::RoomKeyRequest: + return qml_mtx_events::EventType::KeyRequest; + case EventType::RoomAliases: + return qml_mtx_events::EventType::Aliases; + case EventType::RoomAvatar: + return qml_mtx_events::EventType::Avatar; + case EventType::RoomCanonicalAlias: + return qml_mtx_events::EventType::CanonicalAlias; + case EventType::RoomCreate: + return qml_mtx_events::EventType::Create; + case EventType::RoomEncrypted: + return qml_mtx_events::EventType::Encrypted; + case EventType::RoomEncryption: + return qml_mtx_events::EventType::Encryption; + case EventType::RoomGuestAccess: + return qml_mtx_events::EventType::GuestAccess; + case EventType::RoomHistoryVisibility: + return qml_mtx_events::EventType::HistoryVisibility; + case EventType::RoomJoinRules: + return qml_mtx_events::EventType::JoinRules; + case EventType::RoomMember: + return qml_mtx_events::EventType::Member; + case EventType::RoomMessage: + return qml_mtx_events::EventType::UnknownMessage; + case EventType::RoomName: + return qml_mtx_events::EventType::Name; + case EventType::RoomPowerLevels: + return qml_mtx_events::EventType::PowerLevels; + case EventType::RoomTopic: + return qml_mtx_events::EventType::Topic; + case EventType::RoomTombstone: + return qml_mtx_events::EventType::Tombstone; + case EventType::RoomRedaction: + return qml_mtx_events::EventType::Redaction; + case EventType::RoomPinnedEvents: + return qml_mtx_events::EventType::PinnedEvents; + case EventType::Sticker: + return qml_mtx_events::EventType::Sticker; + case EventType::Tag: + return qml_mtx_events::EventType::Tag; + case EventType::Unsupported: + default: + return qml_mtx_events::EventType::Unsupported; + } +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::AudioMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::EmoteMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::FileMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::ImageMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::NoticeMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::TextMessage; +} +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::VideoMessage; +} + +qml_mtx_events::EventType +toRoomEventType(const mtx::events::Event &) +{ + return qml_mtx_events::EventType::Redacted; +} +// ::EventType::Type toRoomEventType(const Event &e) { return +// ::EventType::LocationMessage; } + +template +uint64_t +eventHeight(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventHeight(const mtx::events::RoomEvent &e) -> decltype(e.content.info.h) +{ + return e.content.info.h; +} +template +uint64_t +eventWidth(const mtx::events::Event &) +{ + return -1; +} +template +auto +eventWidth(const mtx::events::RoomEvent &e) -> decltype(e.content.info.w) +{ + return e.content.info.w; +} + +template +double +eventPropHeight(const mtx::events::RoomEvent &e) +{ + auto w = eventWidth(e); + if (w == 0) + w = 1; + return eventHeight(e) / (double)w; +} +} + +TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObject *parent) + : QAbstractListModel(parent) + , room_id_(room_id) + , manager_(manager) +{ + connect( + this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); + connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { + pending.remove(txn_id); + failed.insert(txn_id); + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Failed index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + int idx = idToIndex(txn_id); + if (idx < 0) { + nhlog::ui()->warn("Sent index out of range"); + return; + } + eventOrder[idx] = event_id; + auto ev = events.value(txn_id); + ev = boost::apply_visitor( + [event_id](const auto &e) -> mtx::events::collections::TimelineEvents { + auto eventCopy = e; + eventCopy.event_id = event_id.toStdString(); + return eventCopy; + }, + ev); + events.remove(txn_id); + events.insert(event_id, ev); + + // mark our messages as read + readEvent(event_id.toStdString()); + + // ask to be notified for read receipts + cache::client()->addPendingReceipt(room_id_, event_id); + + emit dataChanged(index(idx, 0), index(idx, 0)); + }); + connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { + emit ChatPage::instance()->showNotification(msg); + }); +} + +QHash +TimelineModel::roleNames() const +{ + return { + {Section, "section"}, + {Type, "type"}, + {Body, "body"}, + {FormattedBody, "formattedBody"}, + {UserId, "userId"}, + {UserName, "userName"}, + {Timestamp, "timestamp"}, + {Url, "url"}, + {ThumbnailUrl, "thumbnailUrl"}, + {Filename, "filename"}, + {Filesize, "filesize"}, + {MimeType, "mimetype"}, + {Height, "height"}, + {Width, "width"}, + {ProportionalHeight, "proportionalHeight"}, + {Id, "id"}, + {State, "state"}, + {IsEncrypted, "isEncrypted"}, + {ReplyTo, "replyTo"}, + }; +} +int +TimelineModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return (int)this->eventOrder.size(); +} + +QVariant +TimelineModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 && index.row() >= (int)eventOrder.size()) + return QVariant(); + + QString id = eventOrder[index.row()]; + + mtx::events::collections::TimelineEvents event = events.value(id); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + switch (role) { + case Section: { + QDateTime date = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event); + date.setTime(QTime()); + + QString userId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); + + for (int r = index.row() - 1; r > 0; r--) { + QDateTime prevDate = boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, + events.value(eventOrder[r])); + prevDate.setTime(QTime()); + if (prevDate != date) + return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); + + QString prevUserId = + boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + events.value(eventOrder[r])); + if (userId != prevUserId) + break; + } + + return QString("%1").arg(userId); + } + case UserId: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event)); + case UserName: + return QVariant(displayName(boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, event))); + + case Timestamp: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, event)); + case Type: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, + event)); + case Body: + return QVariant(utils::replaceEmoji(boost::apply_visitor( + [](const auto &e) -> QString { return eventBody(e); }, event))); + case FormattedBody: + return QVariant( + utils::replaceEmoji( + boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + .remove("") + .remove("")); + case Url: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventUrl(e); }, event)); + case ThumbnailUrl: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventThumbnailUrl(e); }, event)); + case Filename: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventFilename(e); }, event)); + case Filesize: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { + return utils::humanReadableFileSize(eventFilesize(e)); + }, + event)); + case MimeType: + return QVariant(boost::apply_visitor( + [](const auto &e) -> QString { return eventMimeType(e); }, event)); + case Height: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventHeight(e); }, event)); + case Width: + return QVariant(boost::apply_visitor( + [](const auto &e) -> qulonglong { return eventWidth(e); }, event)); + case ProportionalHeight: + return QVariant(boost::apply_visitor( + [](const auto &e) -> double { return eventPropHeight(e); }, event)); + case Id: + return id; + case State: + // only show read receipts for messages not from us + if (boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, + event) + .toStdString() != http::client()->user_id().to_string()) + return qml_mtx_events::Empty; + else if (failed.contains(id)) + return qml_mtx_events::Failed; + else if (pending.contains(id)) + return qml_mtx_events::Sent; + else if (read.contains(id) || + cache::client()->readReceipts(id, room_id_).size() > 1) + return qml_mtx_events::Read; + else + return qml_mtx_events::Received; + case IsEncrypted: { + auto tempEvent = events[id]; + return boost::get>( + &tempEvent) != nullptr; + } + case ReplyTo: { + QString evId = boost::apply_visitor( + [](const auto &e) -> QString { return eventRelatesTo(e); }, event); + return QVariant(evId); + } + default: + return QVariant(); + } +} + +void +TimelineModel::addEvents(const mtx::responses::Timeline &timeline) +{ + if (isInitialSync) { + prev_batch_token_ = QString::fromStdString(timeline.prev_batch); + isInitialSync = false; + } + + if (timeline.events.empty()) + return; + + std::vector ids = internalAddEvents(timeline.events); + + if (ids.empty()) + return; + + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size() + ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.end(), ids.begin(), ids.end()); + endInsertRows(); + + updateLastMessage(); +} + +void +TimelineModel::updateLastMessage() +{ + auto event = events.value(eventOrder.back()); + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); +} + +std::vector +TimelineModel::internalAddEvents( + const std::vector &timeline) +{ + std::vector ids; + for (const auto &e : timeline) { + QString id = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e); + + if (this->events.contains(id)) { + this->events.insert(id, e); + int idx = idToIndex(id); + emit dataChanged(index(idx, 0), index(idx, 0)); + continue; + } + + if (auto redaction = + boost::get>(&e)) { + QString redacts = QString::fromStdString(redaction->redacts); + auto redacted = std::find(eventOrder.begin(), eventOrder.end(), redacts); + + if (redacted != eventOrder.end()) { + auto redactedEvent = boost::apply_visitor( + [](const auto &ev) + -> mtx::events::RoomEvent { + mtx::events::RoomEvent + replacement = {}; + replacement.event_id = ev.event_id; + replacement.room_id = ev.room_id; + replacement.sender = ev.sender; + replacement.origin_server_ts = ev.origin_server_ts; + replacement.type = ev.type; + return replacement; + }, + e); + events.insert(redacts, redactedEvent); + + int row = (int)std::distance(eventOrder.begin(), redacted); + emit dataChanged(index(row, 0), index(row, 0)); + } + + continue; // don't insert redaction into timeline + } + + this->events.insert(id, e); + ids.push_back(id); + } + return ids; +} + +void +TimelineModel::fetchHistory() +{ + if (paginationInProgress) { + nhlog::ui()->warn("Already loading older messages"); + return; + } + + paginationInProgress = true; + mtx::http::MessagesOpts opts; + opts.room_id = room_id_.toStdString(); + opts.from = prev_batch_token_.toStdString(); + + nhlog::ui()->info("Paginationg room {}", opts.room_id); + + http::client()->messages( + opts, [this, opts](const mtx::responses::Messages &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("failed to call /messages ({}): {} - {}", + opts.room_id, + mtx::errors::to_string(err->matrix_error.errcode), + err->matrix_error.error); + paginationInProgress = false; + return; + } + + emit oldMessagesRetrieved(std::move(res)); + paginationInProgress = false; + }); +} + +void +TimelineModel::setCurrentIndex(int index) +{ + auto oldIndex = idToIndex(currentId); + currentId = indexToId(index); + emit currentIndexChanged(index); + + if (oldIndex < index && !pending.contains(currentId)) { + readEvent(currentId.toStdString()); + } +} + +void +TimelineModel::readEvent(const std::string &id) +{ + http::client()->read_event(room_id_.toStdString(), id, [this](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to read_event ({}, {})", + room_id_.toStdString(), + currentId.toStdString()); + } + }); +} + +void +TimelineModel::addBackwardsEvents(const mtx::responses::Messages &msgs) +{ + std::vector ids = internalAddEvents(msgs.chunk); + + if (!ids.empty()) { + beginInsertRows(QModelIndex(), 0, static_cast(ids.size() - 1)); + this->eventOrder.insert(this->eventOrder.begin(), ids.rbegin(), ids.rend()); + endInsertRows(); + } + + prev_batch_token_ = QString::fromStdString(msgs.end); +} + +QColor +TimelineModel::userColor(QString id, QColor background) +{ + if (!userColors.contains(id)) + userColors.insert( + id, QColor(utils::generateContrastingHexColor(id, background.name()))); + return userColors.value(id); +} + +QString +TimelineModel::displayName(QString id) const +{ + return Cache::displayName(room_id_, id); +} + +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + +QString +TimelineModel::formatDateSeparator(QDate date) const +{ + auto now = QDateTime::currentDateTime(); + + QString fmt = QLocale::system().dateFormat(QLocale::LongFormat); + + if (now.date().year() == date.year()) { + QRegularExpression rx("[^a-zA-Z]*y+[^a-zA-Z]*"); + fmt = fmt.remove(rx); + } + + return date.toString(fmt); +} + +QString +TimelineModel::escapeEmoji(QString str) const +{ + return utils::replaceEmoji(str); +} + +void +TimelineModel::viewRawMessage(QString id) const +{ + std::string ev = utils::serialize_event(events.value(id)).dump(4); + auto dialog = new dialogs::RawMessage(QString::fromStdString(ev)); + Q_UNUSED(dialog); +} + +void + +TimelineModel::openUserProfile(QString userid) const +{ + MainWindow::instance()->openUserProfile(userid, room_id_); +} + +DecryptionResult +TimelineModel::decryptEvent(const mtx::events::EncryptedEvent &e) const +{ + MegolmSessionIndex index; + index.room_id = room_id_.toStdString(); + index.session_id = e.content.session_id; + index.sender_key = e.content.sender_key; + + mtx::events::RoomEvent dummy; + dummy.origin_server_ts = e.origin_server_ts; + dummy.event_id = e.event_id; + dummy.sender = e.sender; + dummy.content.body = + tr("-- Encrypted Event (No keys found for decryption) --", + "Placeholder, when the message was not decrypted yet or can't be decrypted") + .toStdString(); + + try { + if (!cache::client()->inboundMegolmSessionExists(index)) { + nhlog::crypto()->info("Could not find inbound megolm session ({}, {}, {})", + index.room_id, + index.session_id, + e.sender); + // TODO: request megolm session_id & session_key from the sender. + return {dummy, false}; + } + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to check megolm session's existence: {}", e.what()); + dummy.content.body = tr("-- Decryption Error (failed to communicate with DB) --", + "Placeholder, when the message can't be decrypted, because " + "the DB access failed when trying to lookup the session.") + .toStdString(); + return {dummy, false}; + } + + std::string msg_str; + try { + auto session = cache::client()->getInboundMegolmSession(index); + auto res = olm::client()->decrypt_group_message(session, e.content.ciphertext); + msg_str = std::string((char *)res.data.data(), res.data.size()); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (failed to retrieve megolm keys from db) --", + "Placeholder, when the message can't be decrypted, because the DB access " + "failed.") + .toStdString(); + return {dummy, false}; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}", + index.room_id, + index.session_id, + index.sender_key, + e.what()); + dummy.content.body = + tr("-- Decryption Error (%1) --", + "Placeholder, when the message can't be decrypted. In this case, the Olm " + "decrytion returned an error, which is passed ad %1") + .arg(e.what()) + .toStdString(); + return {dummy, false}; + } + + // Add missing fields for the event. + json body = json::parse(msg_str); + body["event_id"] = e.event_id; + body["sender"] = e.sender; + body["origin_server_ts"] = e.origin_server_ts; + body["unsigned"] = e.unsigned_data; + + json event_array = json::array(); + event_array.push_back(body); + + std::vector temp_events; + mtx::responses::utils::parse_timeline_events(event_array, temp_events); + + if (temp_events.size() == 1) + return {temp_events.at(0), true}; + + dummy.content.body = + tr("-- Encrypted Event (Unknown event type) --", + "Placeholder, when the message was decrypted, but we couldn't parse it, because " + "Nheko/mtxclient don't support that event type yet") + .toStdString(); + return {dummy, false}; +} + +void +TimelineModel::replyAction(QString id) +{ + auto event = events.value(id); + RelatedInfo related = boost::apply_visitor( + [](const auto &ev) -> RelatedInfo { + RelatedInfo related_ = {}; + related_.quoted_user = QString::fromStdString(ev.sender); + related_.related_event = ev.event_id; + return related_; + }, + event); + related.type = mtx::events::getMessageType(boost::apply_visitor( + [](const auto &e) -> std::string { return eventMsgType(e); }, event)); + related.quoted_body = boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event); + related.quoted_body.remove(QRegularExpression( + ".*", QRegularExpression::DotMatchesEverythingOption)); + nhlog::ui()->debug("after replacement: {}", related.quoted_body.toStdString()); + related.room = room_id_; + + if (related.quoted_body.isEmpty()) + return; + + ChatPage::instance()->messageReply(related); +} + +void +TimelineModel::readReceiptsAction(QString id) const +{ + MainWindow::instance()->openReadReceiptsDialog(id); +} + +void +TimelineModel::redactEvent(QString id) +{ + if (!id.isEmpty()) + http::client()->redact_event( + room_id_.toStdString(), + id.toStdString(), + [this, id](const mtx::responses::EventId &, mtx::http::RequestErr err) { + if (err) { + emit redactionFailed( + tr("Message redaction failed: %1") + .arg(QString::fromStdString(err->matrix_error.error))); + return; + } + + emit eventRedacted(id); + }); +} + +int +TimelineModel::idToIndex(QString id) const +{ + if (id.isEmpty()) + return -1; + for (int i = 0; i < (int)eventOrder.size(); i++) + if (id == eventOrder[i]) + return i; + return -1; +} + +QString +TimelineModel::indexToId(int index) const +{ + if (index < 0 || index >= (int)eventOrder.size()) + return ""; + return eventOrder[index]; +} + +// Note: this will only be called for our messages +void +TimelineModel::markEventsAsRead(const std::vector &event_ids) +{ + for (const auto &id : event_ids) { + read.insert(id); + int idx = idToIndex(id); + if (idx < 0) { + nhlog::ui()->warn("Read index out of range"); + return; + } + emit dataChanged(index(idx, 0), index(idx, 0)); + } +} + +void +TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json content) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client()->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed(QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + return; + } + + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + auto outbound_session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(outbound_session.get()); + const auto session_key = mtx::crypto::session_key(outbound_session.get()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // Saving the new megolm session. + // TODO: Maybe it's too early to save. + OutboundGroupSessionData session_data; + session_data.session_id = session_id; + session_data.session_key = session_key; + session_data.message_index = 0; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = + std::make_shared([megolm_payload, room_id, doc, txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::client()->device_id(), doc.dump()); + + http::client() + ->send_room_message( + room_id, + txn_id, + data, + [this, txn_id](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn( + "[{}] failed to send message: {} {}", + txn_id, + err->matrix_error.error, + status_code); + emit messageFailed( + QString::fromStdString(txn_id)); + } + emit messageSent( + QString::fromStdString(txn_id), + QString::fromStdString(res.event_id.to_string())); + }); + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to save megolm outbound session: {}", e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload, this]( + const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &user : res.device_keys) { + // Mapping from a device_id with valid identity keys to the + // generated room_key event used for sharing the megolm session. + std::map room_key_msgs; + std::map deviceKeys; + + room_key_msgs.clear(); + deviceKeys.clear(); + + for (const auto &dev : user.second) { + const auto user_id = ::UserId(dev.second.user_id); + const auto device_id = DeviceId(dev.second.device_id); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + device_id.get(); + const auto edKey = "ed25519:" + device_id.get(); + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + nhlog::net()->debug( + "ignoring malformed keys for device {}", + device_id.get()); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + try { + if (!mtx::crypto::verify_identity_signature( + json(dev.second), device_id, user_id)) { + nhlog::crypto()->warn( + "failed to verify identity keys: {}", + json(dev.second).dump(2)); + continue; + } + } catch (const json::exception &e) { + nhlog::crypto()->warn( + "failed to parse device key json: {}", + e.what()); + continue; + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->warn( + "failed to verify device key json: {}", + e.what()); + continue; + } + + auto room_key = olm::client() + ->create_room_key_event( + user_id, pks.ed25519, megolm_payload) + .dump(); + + room_key_msgs.emplace(device_id, room_key); + deviceKeys.emplace(device_id, pks); + } + + std::vector valid_devices; + valid_devices.reserve(room_key_msgs.size()); + for (auto const &d : room_key_msgs) { + valid_devices.push_back(d.first); + + nhlog::net()->info("{}", d.first); + nhlog::net()->info(" curve25519 {}", + deviceKeys.at(d.first).curve25519); + nhlog::net()->info(" ed25519 {}", + deviceKeys.at(d.first).ed25519); + } + + nhlog::net()->info( + "sending claim request for user {} with {} devices", + user.first, + valid_devices.size()); + + http::client()->claim_keys( + user.first, + valid_devices, + std::bind(&TimelineModel::handleClaimedKeys, + this, + keeper, + room_key_msgs, + deviceKeys, + user.first, + std::placeholders::_1, + std::placeholders::_2)); + + // TODO: Wait before sending the next batch of requests. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + + // TODO: Let the user know about the errors. + } catch (const lmdb::error &e) { + nhlog::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + } +} + +void +TimelineModel::handleClaimedKeys(std::shared_ptr keeper, + const std::map &room_keys, + const std::map &pks, + const std::string &user_id, + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) +{ + if (err) { + nhlog::net()->warn("claim keys error: {} {} {}", + err->matrix_error.error, + err->parse_error, + static_cast(err->status_code)); + return; + } + + nhlog::net()->debug("claimed keys for {}", user_id); + + if (res.one_time_keys.size() == 0) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) { + nhlog::net()->debug("no one-time keys found for user_id: {}", user_id); + return; + } + + auto retrieved_devices = res.one_time_keys.at(user_id); + + // Payload with all the to_device message to be sent. + json body; + body["messages"][user_id] = json::object(); + + for (const auto &rd : retrieved_devices) { + const auto device_id = rd.first; + nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + + if (pks.find(device_id) == pks.end()) { + nhlog::net()->critical("couldn't find public key for device: {}", + device_id); + continue; + } + + auto id_key = pks.at(device_id).curve25519; + auto s = olm::client()->create_outbound_session(id_key, otk); + + if (room_keys.find(device_id) == room_keys.end()) { + nhlog::net()->critical("couldn't find m.room_key for device: {}", + device_id); + continue; + } + + auto device_msg = olm::client()->create_olm_encrypted_content( + s.get(), room_keys.at(device_id), pks.at(device_id).curve25519); + + try { + cache::client()->saveOlmSession(id_key, std::move(s)); + } catch (const lmdb::error &e) { + nhlog::db()->critical("failed to save outbound olm session: {}", e.what()); + } catch (const mtx::crypto::olm_exception &e) { + nhlog::crypto()->critical("failed to pickle outbound olm session: {}", + e.what()); + } + + body["messages"][user_id][device_id] = device_msg; + } + + nhlog::net()->info("send_to_device: {}", user_id); + + http::client()->send_to_device( + "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to send " + "send_to_device " + "message: {}", + err->matrix_error.error); + } + + (void)keeper; + }); +} -- cgit 1.5.1 From 562169965ce68cadbf8214e084477c60ddfdde0b Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 9 Nov 2019 03:30:17 +0100 Subject: Show only messages in room list --- src/timeline/TimelineModel.cpp | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index ab7d3d47..9cae4608 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -552,17 +552,40 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline) updateLastMessage(); } +template +auto +isMessage(const mtx::events::RoomEvent &e) + -> std::enable_if_t::value, bool> +{ + return true; +} + +template +auto +isMessage(const mtx::events::Event &) +{ + return false; +} + void TimelineModel::updateLastMessage() { - auto event = events.value(eventOrder.back()); - if (auto e = boost::get>(&event)) { - event = decryptEvent(*e).event; - } + for (auto it = eventOrder.rbegin(); it != eventOrder.rend(); ++it) { + auto event = events.value(*it); + if (auto e = boost::get>( + &event)) { + event = decryptEvent(*e).event; + } - auto description = utils::getMessageDescription( - event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); - emit manager_->updateRoomsLastMessage(room_id_, description); + if (!boost::apply_visitor([](const auto &e) -> bool { return isMessage(e); }, + event)) + continue; + + auto description = utils::getMessageDescription( + event, QString::fromStdString(http::client()->user_id().to_string()), room_id_); + emit manager_->updateRoomsLastMessage(room_id_, description); + return; + } } std::vector -- cgit 1.5.1 From c424e397b01d8191568f951bdb754e1957681fb8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 10 Nov 2019 00:30:02 +0100 Subject: Add loading spinner and restore message send queue --- resources/qml/TimelineView.qml | 13 +++-- src/timeline/TimelineModel.cpp | 97 +++++++++++++++++++++++++++++++++++- src/timeline/TimelineModel.h | 41 ++++----------- src/timeline/TimelineViewManager.cpp | 3 ++ src/timeline/TimelineViewManager.h | 14 +++--- 5 files changed, 123 insertions(+), 45 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index b25b3a7c..3bbaa020 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -19,13 +19,20 @@ Item { color: colors.window Text { - visible: !timelineManager.timeline + visible: !timelineManager.timeline && !timelineManager.isInitialSync anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 color: colors.windowText } + BusyIndicator { + anchors.centerIn: parent + running: timelineManager.isInitialSync + height: 200 + width: 200 + } + ListView { id: chat @@ -47,10 +54,6 @@ Item { } else { positionViewAtIndex(model.currentIndex, ListView.End) } - - //if (contentHeight < height) { - // model.fetchHistory(); - //} } } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 9cae4608..6b0057a4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -332,16 +332,18 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj connect( this, &TimelineModel::oldMessagesRetrieved, this, &TimelineModel::addBackwardsEvents); connect(this, &TimelineModel::messageFailed, this, [this](QString txn_id) { - pending.remove(txn_id); + pending.removeOne(txn_id); failed.insert(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Failed index out of range"); return; } + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); }); connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) { + pending.removeOne(txn_id); int idx = idToIndex(txn_id); if (idx < 0) { nhlog::ui()->warn("Sent index out of range"); @@ -365,11 +367,19 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj // ask to be notified for read receipts cache::client()->addPendingReceipt(room_id_, event_id); + isProcessingPending = false; emit dataChanged(index(idx, 0), index(idx, 0)); + + if (pending.size() > 0) + emit nextPendingMessage(); }); connect(this, &TimelineModel::redactionFailed, this, [](const QString &msg) { emit ChatPage::instance()->showNotification(msg); }); + + connect( + this, &TimelineModel::nextPendingMessage, this, &TimelineModel::processOnePendingMessage); + connect(this, &TimelineModel::newMessageToSend, this, &TimelineModel::addPendingMessage); } QHash @@ -1035,6 +1045,7 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to save megolm outbound session: {}", e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } }); @@ -1044,13 +1055,14 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co http::client()->query_keys( req, - [keeper = std::move(keeper), megolm_payload, this]( + [keeper = std::move(keeper), megolm_payload, txn_id, this]( const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) { if (err) { nhlog::net()->warn("failed to query device keys: {} {}", err->matrix_error.error, static_cast(err->status_code)); // TODO: Mark the event as failed. Communicate with the UI. + emit messageFailed(QString::fromStdString(txn_id)); return; } @@ -1150,9 +1162,11 @@ TimelineModel::sendEncryptedMessage(const std::string &txn_id, nlohmann::json co } catch (const lmdb::error &e) { nhlog::db()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } catch (const mtx::crypto::olm_exception &e) { nhlog::crypto()->critical( "failed to open outbound megolm session ({}): {}", room_id, e.what()); + emit messageFailed(QString::fromStdString(txn_id)); } } @@ -1241,3 +1255,82 @@ TimelineModel::handleClaimedKeys(std::shared_ptr keeper, (void)keeper; }); } + +struct SendMessageVisitor +{ + SendMessageVisitor(const QString &txn_id, TimelineModel *model) + : txn_id_qstr_(txn_id) + , model_(model) + {} + + template + void operator()(const mtx::events::Event &) + {} + + template::value, int> = 0> + void operator()(const mtx::events::RoomEvent &msg) + + { + if (cache::client()->isRoomEncrypted(model_->room_id_.toStdString())) { + model_->sendEncryptedMessage(txn_id_qstr_.toStdString(), + nlohmann::json(msg.content)); + } else { + QString txn_id_qstr = txn_id_qstr_; + TimelineModel *model = model_; + http::client()->send_room_message( + model->room_id_.toStdString(), + txn_id_qstr.toStdString(), + msg.content, + [txn_id_qstr, model](const mtx::responses::EventId &res, + mtx::http::RequestErr err) { + if (err) { + const int status_code = + static_cast(err->status_code); + nhlog::net()->warn("[{}] failed to send message: {} {}", + txn_id_qstr.toStdString(), + err->matrix_error.error, + status_code); + emit model->messageFailed(txn_id_qstr); + } + emit model->messageSent( + txn_id_qstr, QString::fromStdString(res.event_id.to_string())); + }); + } + } + + QString txn_id_qstr_; + TimelineModel *model_; +}; + +void +TimelineModel::processOnePendingMessage() +{ + if (isProcessingPending || pending.isEmpty()) + return; + + isProcessingPending = true; + + QString txn_id_qstr = pending.first(); + + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, events.value(txn_id_qstr)); +} + +void +TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) +{ + internalAddEvents({event}); + + QString txn_id_qstr = + boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, event); + beginInsertRows(QModelIndex(), + static_cast(this->eventOrder.size()), + static_cast(this->eventOrder.size())); + pending.push_back(txn_id_qstr); + this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); + endInsertRows(); + updateLastMessage(); + + if (!isProcessingPending) + emit nextPendingMessage(); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 31e41315..e7842b99 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -173,6 +173,8 @@ public slots: private slots: // Add old events at the top of the timeline. void addBackwardsEvents(const mtx::responses::Messages &msgs); + void processOnePendingMessage(); + void addPendingMessage(mtx::events::collections::TimelineEvents event); signals: void oldMessagesRetrieved(const mtx::responses::Messages &res); @@ -181,6 +183,8 @@ signals: void currentIndexChanged(int index); void redactionFailed(QString id); void eventRedacted(QString id); + void nextPendingMessage(); + void newMessageToSend(mtx::events::collections::TimelineEvents event); private: DecryptionResult decryptEvent( @@ -198,7 +202,8 @@ private: void readEvent(const std::string &id); QHash events; - QSet pending, failed, read; + QSet failed, read; + QList pending; std::vector eventOrder; QString room_id_; @@ -206,11 +211,14 @@ private: bool isInitialSync = true; bool paginationInProgress = false; + bool isProcessingPending = false; QHash userColors; QString currentId; TimelineViewManager *manager_; + + friend struct SendMessageVisitor; }; template @@ -224,35 +232,6 @@ TimelineModel::sendMessage(const T &msg) msgCopy.event_id = txn_id; msgCopy.sender = http::client()->user_id().to_string(); msgCopy.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - internalAddEvents({msgCopy}); - - QString txn_id_qstr = QString::fromStdString(txn_id); - beginInsertRows(QModelIndex(), - static_cast(this->eventOrder.size()), - static_cast(this->eventOrder.size())); - pending.insert(txn_id_qstr); - this->eventOrder.insert(this->eventOrder.end(), txn_id_qstr); - endInsertRows(); - updateLastMessage(); - if (cache::client()->isRoomEncrypted(room_id_.toStdString())) - sendEncryptedMessage(txn_id, nlohmann::json(msg)); - else - http::client()->send_room_message( - room_id_.toStdString(), - txn_id, - msg, - [this, txn_id, txn_id_qstr](const mtx::responses::EventId &res, - mtx::http::RequestErr err) { - if (err) { - const int status_code = static_cast(err->status_code); - nhlog::net()->warn("[{}] failed to send message: {} {}", - txn_id, - err->matrix_error.error, - status_code); - emit messageFailed(txn_id_qstr); - } - emit messageSent(txn_id_qstr, - QString::fromStdString(res.event_id.to_string())); - }); + emit newMessageToSend(msgCopy); } diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index d733ad90..06c42a39 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -97,6 +97,9 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) addRoom(QString::fromStdString(it->first)); models.value(QString::fromStdString(it->first))->addEvents(it->second.timeline); } + + this->isInitialSync_ = false; + emit initialSyncChanged(false); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 691c8ddb..0bc58e68 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -12,10 +12,6 @@ #include "TimelineModel.h" #include "Utils.h" -// temporary for stubs -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" - class MxcImageProvider; class ColorImageProvider; @@ -25,6 +21,8 @@ class TimelineViewManager : public QObject Q_PROPERTY( TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged) + Q_PROPERTY( + bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged) public: TimelineViewManager(QWidget *parent = 0); @@ -36,6 +34,7 @@ public: void clearAll() { models.clear(); } Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } + Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } void openImageOverlay(QString mxcUrl, QString originalFilename, QString mimeType, @@ -66,6 +65,7 @@ signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); + void initialSyncChanged(bool isInitialSync); void mediaCached(QString mxcUrl, QString cacheUrl); public slots: @@ -107,11 +107,11 @@ private: QQuickWidget *view; #endif QWidget *container; - TimelineModel *timeline_ = nullptr; + MxcImageProvider *imgProvider; ColorImageProvider *colorImgProvider; QHash> models; + TimelineModel *timeline_ = nullptr; + bool isInitialSync_ = true; }; - -#pragma GCC diagnostic pop -- cgit 1.5.1 From 001c94865c98836b06c827ff890a5589dd97320d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 15:10:59 +0000 Subject: Fix windows build No idea, why apply visitor doesn't work with temporaries? --- src/dialogs/RoomSettings.cpp | 2 +- src/timeline/TimelineModel.cpp | 8 +++++--- src/timeline/TimelineViewManager.cpp | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/dialogs/RoomSettings.cpp b/src/dialogs/RoomSettings.cpp index 00b034cc..25909cd8 100644 --- a/src/dialogs/RoomSettings.cpp +++ b/src/dialogs/RoomSettings.cpp @@ -488,7 +488,7 @@ RoomSettings::retrieveRoomInfo() usesEncryption_ = cache::client()->isRoomEncrypted(room_id_.toStdString()); info_ = cache::client()->singleRoomInfo(room_id_.toStdString()); setAvatar(); - } catch (const lmdb::error &e) { + } catch (const lmdb::error &) { nhlog::db()->warn("failed to retrieve room info from cache: {}", room_id_.toStdString()); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 6b0057a4..39abbf6f 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -438,16 +438,17 @@ TimelineModel::data(const QModelIndex &index, int role) const boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, event); for (int r = index.row() - 1; r > 0; r--) { + auto tempEv = events.value(eventOrder[r]); QDateTime prevDate = boost::apply_visitor( [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - events.value(eventOrder[r])); + tempEv); prevDate.setTime(QTime()); if (prevDate != date) return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); QString prevUserId = boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - events.value(eventOrder[r])); + tempEv); if (userId != prevUserId) break; } @@ -1313,7 +1314,8 @@ TimelineModel::processOnePendingMessage() QString txn_id_qstr = pending.first(); - boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, events.value(txn_id_qstr)); + auto event = events.value(txn_id_qstr); + boost::apply_visitor(SendMessageVisitor{txn_id_qstr, this}, event); } void diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 06c42a39..39bdfcf4 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -200,7 +200,7 @@ TimelineViewManager::saveMedia(QString mxcUrl, if (!file.open(QIODevice::WriteOnly)) return; - file.write(QByteArray(data.data(), data.size())); + file.write(QByteArray(data.data(), (int)data.size())); file.close(); } catch (const std::exception &e) { nhlog::ui()->warn("Error while saving file to: {}", e.what()); -- cgit 1.5.1 From cf88499ccb6709db3312cd675c87614389cc0aac Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 11 Nov 2019 19:58:20 +0100 Subject: Fix replies to encrypted events --- src/timeline/TimelineModel.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 39abbf6f..72c107d4 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -869,7 +869,11 @@ TimelineModel::decryptEvent(const mtx::events::EncryptedEvent>(&event)) { + event = decryptEvent(*e).event; + } + RelatedInfo related = boost::apply_visitor( [](const auto &ev) -> RelatedInfo { RelatedInfo related_ = {}; -- cgit 1.5.1 From 5429b425e97f3482e7e5510a8606659168f58b7d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Nov 2019 17:46:09 +0100 Subject: Lint --- src/timeline/TimelineModel.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 72c107d4..7c78e552 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -440,15 +440,13 @@ TimelineModel::data(const QModelIndex &index, int role) const for (int r = index.row() - 1; r > 0; r--) { auto tempEv = events.value(eventOrder[r]); QDateTime prevDate = boost::apply_visitor( - [](const auto &e) -> QDateTime { return eventTimestamp(e); }, - tempEv); + [](const auto &e) -> QDateTime { return eventTimestamp(e); }, tempEv); prevDate.setTime(QTime()); if (prevDate != date) return QString("%2 %1").arg(date.toMSecsSinceEpoch()).arg(userId); - QString prevUserId = - boost::apply_visitor([](const auto &e) -> QString { return senderId(e); }, - tempEv); + QString prevUserId = boost::apply_visitor( + [](const auto &e) -> QString { return senderId(e); }, tempEv); if (userId != prevUserId) break; } -- cgit 1.5.1 From 7bd875004f6995865a71a55facf56834c91bfb48 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 16:36:45 +0100 Subject: Only mark messages as read, when room is active --- src/timeline/TimelineModel.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 7c78e552..11344e60 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -685,7 +685,8 @@ TimelineModel::setCurrentIndex(int index) currentId = indexToId(index); emit currentIndexChanged(index); - if (oldIndex < index && !pending.contains(currentId)) { + if (oldIndex < index && !pending.contains(currentId) && + ChatPage::instance()->isActiveWindow()) { readEvent(currentId.toStdString()); } } -- cgit 1.5.1 From 9fd279c020bba2f433a0f9862277bc59fd621130 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 22 Nov 2019 17:08:32 +0100 Subject: Show encryption enabled and use a non zero size for zero size vide --- resources/qml/TimelineView.qml | 2 +- resources/qml/delegates/MessageDelegate.qml | 10 +++++++++- resources/qml/delegates/Pill.qml | 14 ++++++++++++++ resources/qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/Redacted.qml | 15 --------------- resources/res.qrc | 2 +- src/timeline/TimelineModel.cpp | 5 ++++- 7 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 resources/qml/delegates/Pill.qml delete mode 100644 resources/qml/delegates/Redacted.qml (limited to 'src/timeline/TimelineModel.cpp') diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 3bbaa020..a5520031 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -79,7 +79,7 @@ Item { } } - onAtYBeginningChanged: if (atYBeginning) model.fetchHistory() + onAtYBeginningChanged: if (atYBeginning) { chat.model.currentIndex = 0; chat.currentIndex = 0; model.fetchHistory(); } function updatePosition() { for (var y = chat.contentY + chat.height; y > chat.height; y -= 9) { diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 49209f68..e31321f9 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -39,7 +39,15 @@ DelegateChooser { } DelegateChoice { roleValue: MtxEvent.Redacted - Redacted {} + Pill { + text: qsTr("redacted") + } + } + DelegateChoice { + roleValue: MtxEvent.Encryption + Pill { + text: qsTr("Encryption enabled") + } } DelegateChoice { Placeholder {} diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml new file mode 100644 index 00000000..53a9684e --- /dev/null +++ b/resources/qml/delegates/Pill.qml @@ -0,0 +1,14 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.1 + +Label { + color: inactiveColors.text + horizontalAlignment: Text.AlignHCenter + + height: contentHeight * 1.2 + width: contentWidth * 1.2 + background: Rectangle { + radius: parent.height / 2 + color: colors.dark + } +} diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 68b09f7b..1207ac77 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -20,7 +20,7 @@ Rectangle { Rectangle { id: videoContainer visible: model.type == MtxEvent.VideoMessage - width: Math.min(parent.width, model.width) + width: Math.min(parent.width, model.width ? model.width : 400) // some media has 0 as size... height: width*model.proportionalHeight Image { anchors.fill: parent diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml deleted file mode 100644 index 42fb4835..00000000 --- a/resources/qml/delegates/Redacted.qml +++ /dev/null @@ -1,15 +0,0 @@ -import QtQuick 2.5 -import QtQuick.Controls 2.1 - -Label { - text: qsTr("redacted") - color: inactiveColors.text - horizontalAlignment: Text.AlignHCenter - - height: contentHeight * 1.2 - width: contentWidth * 1.2 - background: Rectangle { - radius: parent.height / 2 - color: colors.dark - } -} diff --git a/resources/res.qrc b/resources/res.qrc index c9938d57..53406c48 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -128,7 +128,7 @@ qml/delegates/ImageMessage.qml qml/delegates/PlayableMediaMessage.qml qml/delegates/FileMessage.qml - qml/delegates/Redacted.qml + qml/delegates/Pill.qml qml/delegates/Placeholder.qml diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 11344e60..b904dfd7 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -320,7 +320,10 @@ eventPropHeight(const mtx::events::RoomEvent &e) auto w = eventWidth(e); if (w == 0) w = 1; - return eventHeight(e) / (double)w; + + double prop = eventHeight(e) / (double)w; + + return prop > 0 ? prop : 1.; } } -- cgit 1.5.1 From b8f6e4ce6462f074c34a8b7a286cbabe0e2897aa Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 02:26:41 +0100 Subject: Add encrypted file download --- deps/CMakeLists.txt | 4 +- resources/qml/TimelineRow.qml | 2 +- resources/qml/delegates/FileMessage.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 4 +- src/timeline/TimelineModel.cpp | 184 +++++++++++++++++++++++ src/timeline/TimelineModel.h | 3 + src/timeline/TimelineViewManager.cpp | 154 ++----------------- src/timeline/TimelineViewManager.h | 27 +--- 9 files changed, 210 insertions(+), 172 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt index d0a715e0..c5932ab7 100644 --- a/deps/CMakeLists.txt +++ b/deps/CMakeLists.txt @@ -46,10 +46,10 @@ set(BOOST_SHA256 set( MTXCLIENT_URL - https://github.com/Nheko-Reborn/mtxclient/archive/6eee767cc25a9db9f125843e584656cde1ebb6c5.tar.gz + https://github.com/Nheko-Reborn/mtxclient/archive/f719236b08d373d9508f2467bbfc6dfa953b1f8d.zip ) set(MTXCLIENT_HASH - 72fe77da4fed98b3cf069299f66092c820c900359a27ec26070175f9ad208a03) + 0660756c16cf297e02b0b29c07a59fc851723cc65f305893ae7238e6dd2e41c8) set( TWEENY_URL https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 4917e893..2c2ed02a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -97,7 +97,7 @@ RowLayout { MenuItem { visible: model.type == MtxEvent.ImageMessage || model.type == MtxEvent.VideoMessage || model.type == MtxEvent.AudioMessage || model.type == MtxEvent.FileMessage || model.type == MtxEvent.Sticker text: qsTr("Save as") - onTriggered: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onTriggered: timelineManager.timeline.saveMedia(model.id) } } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index f4cf3f15..2c911c5e 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -31,7 +31,7 @@ Rectangle { } MouseArea { anchors.fill: parent - onClicked: timelineManager.saveMedia(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.timeline.saveMedia(model.id) cursorShape: Qt.PointingHandCursor } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index a1a06012..1b6e5729 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -17,7 +17,7 @@ Item { MouseArea { enabled: model.type == MtxEvent.ImageMessage anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.url, model.filename, model.mimetype, model.type) + onClicked: timelineManager.openImageOverlay(model.url, model.id) } } } diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 3b987545..d0d4d7cb 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -97,7 +97,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.cacheMedia(model.url, model.mimetype); break; + case "": timelineManager.timeline.cacheMedia(model.id); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -118,7 +118,7 @@ Rectangle { } Connections { - target: timelineManager + target: timelineManager.timeline onMediaCached: { if (mxcUrl == model.url) { media.source = "file://" + cacheUrl diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b904dfd7..f606b603 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -3,11 +3,15 @@ #include #include +#include +#include #include +#include #include "ChatPage.h" #include "Logging.h" #include "MainWindow.h" +#include "MxcImageProvider.h" #include "Olm.h" #include "TimelineViewManager.h" #include "Utils.h" @@ -88,17 +92,42 @@ eventFormattedBody(const mtx::events::RoomEvent &e) } } +template +boost::optional +eventEncryptionInfo(const mtx::events::Event &) +{ + return boost::none; +} + +template +auto +eventEncryptionInfo(const mtx::events::RoomEvent &e) -> std::enable_if_t< + std::is_same>::value, + boost::optional> +{ + return e.content.file; +} + template QString eventUrl(const mtx::events::Event &) { return ""; } + +QString +eventUrl(const mtx::events::StateEvent &e) +{ + return QString::fromStdString(e.content.url); +} + template auto eventUrl(const mtx::events::RoomEvent &e) -> std::enable_if_t::value, QString> { + if (e.content.file) + return QString::fromStdString(e.content.file->url); return QString::fromStdString(e.content.url); } @@ -1342,3 +1371,158 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event) if (!isProcessingPending) emit nextPendingMessage(); } + +void +TimelineModel::saveMedia(QString eventId) const +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString originalFilename = + boost::apply_visitor([](const auto &e) -> QString { return eventFilename(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + qml_mtx_events::EventType eventType = boost::apply_visitor( + [](const auto &e) -> qml_mtx_events::EventType { return toRoomEventType(e); }, event); + + QString dialogTitle; + if (eventType == qml_mtx_events::EventType::ImageMessage) { + dialogTitle = tr("Save image"); + } else if (eventType == qml_mtx_events::EventType::VideoMessage) { + dialogTitle = tr("Save video"); + } else if (eventType == qml_mtx_events::EventType::AudioMessage) { + dialogTitle = tr("Save audio"); + } else { + dialogTitle = tr("Save file"); + } + + QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); + + auto filename = QFileDialog::getSaveFileName( + manager_->getWidget(), dialogTitle, originalFilename, filterString); + + if (filename.isEmpty()) + return; + + const auto url = mxcUrl.toStdString(); + + http::client()->download( + url, + [filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), (int)temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); +} + +void +TimelineModel::cacheMedia(QString eventId) +{ + mtx::events::collections::TimelineEvents event = events.value(eventId); + + if (auto e = boost::get>(&event)) { + event = decryptEvent(*e).event; + } + + QString mxcUrl = + boost::apply_visitor([](const auto &e) -> QString { return eventUrl(e); }, event); + QString mimeType = + boost::apply_visitor([](const auto &e) -> QString { return eventMimeType(e); }, event); + + using EncF = boost::optional; + EncF encryptionInfo = + boost::apply_visitor([](const auto &e) -> EncF { return eventEncryptionInfo(e); }, event); + + // If the message is a link to a non mxcUrl, don't download it + if (!mxcUrl.startsWith("mxc://")) { + emit mediaCached(mxcUrl, mxcUrl); + return; + } + + QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); + + const auto url = mxcUrl.toStdString(); + QFileInfo filename(QString("%1/media_cache/%2.%3") + .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) + .arg(QString(mxcUrl).remove("mxc://")) + .arg(suffix)); + if (QDir::cleanPath(filename.path()) != filename.path()) { + nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); + return; + } + + QDir().mkpath(filename.path()); + + if (filename.isReadable()) { + emit mediaCached(mxcUrl, filename.filePath()); + return; + } + + http::client()->download( + url, + [this, mxcUrl, filename, url, encryptionInfo](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + auto temp = data; + if (encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, encryptionInfo.value())); + + QFile file(filename.filePath()); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(temp.data(), temp.size())); + file.close(); + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + + emit mediaCached(mxcUrl, filename.filePath()); + }); +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index e7842b99..f52091e6 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -159,6 +159,8 @@ public: Q_INVOKABLE void redactEvent(QString id); Q_INVOKABLE int idToIndex(QString id) const; Q_INVOKABLE QString indexToId(int index) const; + Q_INVOKABLE void cacheMedia(QString eventId); + Q_INVOKABLE void saveMedia(QString eventId) const; void addEvents(const mtx::responses::Timeline &events); template @@ -185,6 +187,7 @@ signals: void eventRedacted(QString id); void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); + void mediaCached(QString mxcUrl, QString cacheUrl); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 2a88c882..6430a426 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -1,11 +1,8 @@ #include "TimelineViewManager.h" -#include #include -#include #include #include -#include #include "ChatPage.h" #include "ColorImageProvider.h" @@ -124,146 +121,24 @@ TimelineViewManager::setHistoryView(const QString &room_id) } void -TimelineViewManager::openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const +TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const { QQuickImageResponse *imgResponse = imgProvider->requestImageResponse(mxcUrl.remove("mxc://"), QSize()); - connect(imgResponse, - &QQuickImageResponse::finished, - this, - [this, mxcUrl, originalFilename, mimeType, eventType, imgResponse]() { - if (!imgResponse->errorString().isEmpty()) { - nhlog::ui()->error("Error when retrieving image for overlay: {}", - imgResponse->errorString().toStdString()); - return; - } - auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->show(); - connect(imgDialog, - &dialogs::ImageOverlay::saving, - this, - [this, mxcUrl, originalFilename, mimeType, eventType]() { - saveMedia(mxcUrl, originalFilename, mimeType, eventType); - }); - }); -} - -void -TimelineViewManager::saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const -{ - QString dialogTitle; - if (eventType == qml_mtx_events::EventType::ImageMessage) { - dialogTitle = tr("Save image"); - } else if (eventType == qml_mtx_events::EventType::VideoMessage) { - dialogTitle = tr("Save video"); - } else if (eventType == qml_mtx_events::EventType::AudioMessage) { - dialogTitle = tr("Save audio"); - } else { - dialogTitle = tr("Save file"); - } - - QString filterString = QMimeDatabase().mimeTypeForName(mimeType).filterString(); - - auto filename = - QFileDialog::getSaveFileName(container, dialogTitle, originalFilename, filterString); - - if (filename.isEmpty()) - return; - - const auto url = mxcUrl.toStdString(); - - http::client()->download( - url, - [filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), (int)data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - }); -} - -void -TimelineViewManager::cacheMedia(QString mxcUrl, QString mimeType) -{ - // If the message is a link to a non mxcUrl, don't download it - if (!mxcUrl.startsWith("mxc://")) { - emit mediaCached(mxcUrl, mxcUrl); - return; - } - - QString suffix = QMimeDatabase().mimeTypeForName(mimeType).preferredSuffix(); - - const auto url = mxcUrl.toStdString(); - QFileInfo filename(QString("%1/media_cache/%2.%3") - .arg(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)) - .arg(QString(mxcUrl).remove("mxc://")) - .arg(suffix)); - if (QDir::cleanPath(filename.path()) != filename.path()) { - nhlog::net()->warn("mxcUrl '{}' is not safe, not downloading file", url); - return; - } - - QDir().mkpath(filename.path()); - - if (filename.isReadable()) { - emit mediaCached(mxcUrl, filename.filePath()); - return; - } + connect(imgResponse, &QQuickImageResponse::finished, this, [this, eventId, imgResponse]() { + if (!imgResponse->errorString().isEmpty()) { + nhlog::ui()->error("Error when retrieving image for overlay: {}", + imgResponse->errorString().toStdString()); + return; + } + auto pixmap = QPixmap::fromImage(imgResponse->textureFactory()->image()); - http::client()->download( - url, - [this, mxcUrl, filename, url](const std::string &data, - const std::string &, - const std::string &, - mtx::http::RequestErr err) { - if (err) { - nhlog::net()->warn("failed to retrieve image {}: {} {}", - url, - err->matrix_error.error, - static_cast(err->status_code)); - return; - } - - try { - QFile file(filename.filePath()); - - if (!file.open(QIODevice::WriteOnly)) - return; - - file.write(QByteArray(data.data(), data.size())); - file.close(); - } catch (const std::exception &e) { - nhlog::ui()->warn("Error while saving file to: {}", e.what()); - } - - emit mediaCached(mxcUrl, filename.filePath()); - }); + auto imgDialog = new dialogs::ImageOverlay(pixmap); + imgDialog->show(); + connect(imgDialog, &dialogs::ImageOverlay::saving, timeline_, [this, eventId]() { + timeline_->saveMedia(eventId); + }); + }); } void @@ -401,3 +276,4 @@ TimelineViewManager::queueVideoMessage(const QString &roomid, video.url = url.toStdString(); models.value(roomid)->sendMessage(video); } + diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 0bc58e68..1cb0de44 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -35,38 +35,13 @@ public: Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; } Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } - void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - qml_mtx_events::EventType eventType) const; - Q_INVOKABLE void cacheMedia(QString mxcUrl, QString mimeType); - // Qml can only pass enum as int - Q_INVOKABLE void openImageOverlay(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - openImageOverlay( - mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } - Q_INVOKABLE void saveMedia(QString mxcUrl, - QString originalFilename, - QString mimeType, - int eventType) const - { - saveMedia(mxcUrl, originalFilename, mimeType, (qml_mtx_events::EventType)eventType); - } + Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info); void activeTimelineChanged(TimelineModel *timeline); void initialSyncChanged(bool isInitialSync); - void mediaCached(QString mxcUrl, QString cacheUrl); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); -- cgit 1.5.1 From 5bfdaff7780bc4299c3edab85c688eebf21f7d4e Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 3 Dec 2019 23:34:16 +0100 Subject: Implement decryption of images It is a bit of a hack, but it works... --- CMakeLists.txt | 1 + src/MxcImageProvider.cpp | 9 +++++++-- src/MxcImageProvider.h | 30 ++++++++++++++++++++++++++---- src/timeline/TimelineModel.cpp | 13 +++++++++++++ src/timeline/TimelineModel.h | 2 ++ src/timeline/TimelineViewManager.cpp | 11 ++++++++--- 6 files changed, 57 insertions(+), 9 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/CMakeLists.txt b/CMakeLists.txt index c918d834..67a1dfb0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -365,6 +365,7 @@ qt5_wrap_cpp(MOC_HEADERS src/CommunitiesList.h src/LoginPage.h src/MainWindow.h + src/MxcImageProvider.h src/InviteeItem.h src/QuickSwitcher.h src/RegisterPage.h diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp index 556b019b..edf6ceb5 100644 --- a/src/MxcImageProvider.cpp +++ b/src/MxcImageProvider.cpp @@ -5,7 +5,7 @@ void MxcImageResponse::run() { - if (m_requestedSize.isValid()) { + if (m_requestedSize.isValid() && !m_encryptionInfo) { QString fileName = QString("%1_%2x%3_crop") .arg(m_id) .arg(m_requestedSize.width()) @@ -65,7 +65,12 @@ MxcImageResponse::run() return; } - auto data = QByteArray(res.data(), res.size()); + auto temp = res; + if (m_encryptionInfo) + temp = mtx::crypto::to_string( + mtx::crypto::decrypt_file(temp, m_encryptionInfo.value())); + + auto data = QByteArray(temp.data(), temp.size()); m_image.loadFromData(data); m_image.setText("original filename", QString::fromStdString(originalFilename)); diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h index 19d8a74e..2c197a13 100644 --- a/src/MxcImageProvider.h +++ b/src/MxcImageProvider.h @@ -6,14 +6,21 @@ #include #include +#include + +#include + class MxcImageResponse : public QQuickImageResponse , public QRunnable { public: - MxcImageResponse(const QString &id, const QSize &requestedSize) + MxcImageResponse(const QString &id, + const QSize &requestedSize, + boost::optional encryptionInfo) : m_id(id) , m_requestedSize(requestedSize) + , m_encryptionInfo(encryptionInfo) { setAutoDelete(false); } @@ -29,19 +36,34 @@ public: QString m_id, m_error; QSize m_requestedSize; QImage m_image; + boost::optional m_encryptionInfo; }; -class MxcImageProvider : public QQuickAsyncImageProvider +class MxcImageProvider + : public QObject + , public QQuickAsyncImageProvider { -public: + Q_OBJECT +public slots: QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override { - MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + boost::optional info; + auto temp = infos.find("mxc://" + id); + if (temp != infos.end()) + info = *temp; + + MxcImageResponse *response = new MxcImageResponse(id, requestedSize, info); pool.start(response); return response; } + void addEncryptionInfo(mtx::crypto::EncryptedFile info) + { + infos.insert(QString::fromStdString(info.url), info); + } + private: QThreadPool pool; + QHash infos; }; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f606b603..2c58e2f5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -673,6 +673,19 @@ TimelineModel::internalAddEvents( continue; // don't insert redaction into timeline } + if (auto event = + boost::get>(&e)) { + auto temp = decryptEvent(*event).event; + auto encInfo = boost::apply_visitor( + [](const auto &ev) -> boost::optional { + return eventEncryptionInfo(ev); + }, + temp); + + if (encInfo) + emit newEncryptedImage(encInfo.value()); + } + this->events.insert(id, e); ids.push_back(id); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index f52091e6..06c64acf 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -6,6 +6,7 @@ #include #include +#include #include #include "Cache.h" @@ -188,6 +189,7 @@ signals: void nextPendingMessage(); void newMessageToSend(mtx::events::collections::TimelineEvents event); void mediaCached(QString mxcUrl, QString cacheUrl); + void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); private: DecryptionResult decryptEvent( diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index c44bcbbf..25f72a6d 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -102,9 +102,14 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms) void TimelineViewManager::addRoom(const QString &room_id) { - if (!models.contains(room_id)) - models.insert(room_id, - QSharedPointer(new TimelineModel(this, room_id))); + if (!models.contains(room_id)) { + QSharedPointer newRoom(new TimelineModel(this, room_id)); + connect(newRoom.data(), + &TimelineModel::newEncryptedImage, + imgProvider, + &MxcImageProvider::addEncryptionInfo); + models.insert(room_id, std::move(newRoom)); + } } void -- cgit 1.5.1 From 362efbf5b9ca4d3162d4fc7035959ce1da9f1c94 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 6 Dec 2019 02:56:53 +0100 Subject: Restore linkification of messages --- src/timeline/TimelineModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 2c58e2f5..e3d87ae6 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -505,8 +505,8 @@ TimelineModel::data(const QModelIndex &index, int role) const case FormattedBody: return QVariant( utils::replaceEmoji( - boost::apply_visitor( - [](const auto &e) -> QString { return eventFormattedBody(e); }, event)) + utils::linkifyMessage(boost::apply_visitor( + [](const auto &e) -> QString { return eventFormattedBody(e); }, event))) .remove("") .remove("")); case Url: -- cgit 1.5.1 From e98a61fea60cf3b95441ce3d9591ced0cf93f566 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 10 Dec 2019 14:46:52 +0100 Subject: Show topic and name changes in timeline --- resources/qml/delegates/MessageDelegate.qml | 12 +++++++++++ resources/qml/delegates/NoticeMessage.qml | 3 ++- src/timeline/TimelineModel.cpp | 32 +++++++++++++++++++++++++++++ src/timeline/TimelineModel.h | 2 ++ 4 files changed, 48 insertions(+), 1 deletion(-) (limited to 'src/timeline/TimelineModel.cpp') diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 178dfd86..20ec71e5 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -49,6 +49,18 @@ DelegateChooser { text: qsTr("Encryption enabled") } } + DelegateChoice { + roleValue: MtxEvent.Name + NoticeMessage { + notice: model.roomName ? qsTr("room name changed to: %1").arg(model.roomName) : qsTr("removed room name") + } + } + DelegateChoice { + roleValue: MtxEvent.Topic + NoticeMessage { + notice: model.roomTopic ? qsTr("topic changed to: %1").arg(model.roomTopic) : qsTr("removed topic") + } + } DelegateChoice { Placeholder {} } diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml index a392eb5b..f7467eca 100644 --- a/resources/qml/delegates/NoticeMessage.qml +++ b/resources/qml/delegates/NoticeMessage.qml @@ -1,7 +1,8 @@ import ".." MatrixText { - text: model.formattedBody + property string notice: model.formattedBody.replace("
", "
")
+	text: notice
 	width: parent ? parent.width : undefined
 	font.italic: true
 	color: inactiveColors.text
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index e3d87ae6..9da8a194 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -59,6 +59,30 @@ eventMsgType(const mtx::events::RoomEvent &e) -> decltype(e.content.msgtype)
         return e.content.msgtype;
 }
 
+template
+QString
+eventRoomName(const T &)
+{
+        return "";
+}
+QString
+eventRoomName(const mtx::events::StateEvent &e)
+{
+        return QString::fromStdString(e.content.name);
+}
+
+template
+QString
+eventRoomTopic(const T &)
+{
+        return "";
+}
+QString
+eventRoomTopic(const mtx::events::StateEvent &e)
+{
+        return QString::fromStdString(e.content.topic);
+}
+
 template
 QString
 eventBody(const mtx::events::Event &)
@@ -437,6 +461,8 @@ TimelineModel::roleNames() const
           {State, "state"},
           {IsEncrypted, "isEncrypted"},
           {ReplyTo, "replyTo"},
+          {RoomName, "roomName"},
+          {RoomTopic, "roomTopic"},
         };
 }
 int
@@ -563,6 +589,12 @@ TimelineModel::data(const QModelIndex &index, int role) const
                   [](const auto &e) -> QString { return eventRelatesTo(e); }, event);
                 return QVariant(evId);
         }
+        case RoomName:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRoomName(e); }, event));
+        case RoomTopic:
+                return QVariant(boost::apply_visitor(
+                  [](const auto &e) -> QString { return eventRoomTopic(e); }, event));
         default:
                 return QVariant();
         }
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 06c64acf..05e05962 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -141,6 +141,8 @@ public:
                 State,
                 IsEncrypted,
                 ReplyTo,
+                RoomName,
+                RoomTopic,
         };
 
         QHash roleNames() const override;
-- 
cgit 1.5.1


From 9c1912ed93cc0628c578a005f0c06bed7b88f63d Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Fri, 13 Dec 2019 01:31:14 +0100
Subject: Fix preview of sent encrypted images

---
 src/timeline/TimelineModel.cpp | 21 +++++++++++----------
 1 file changed, 11 insertions(+), 10 deletions(-)

(limited to 'src/timeline/TimelineModel.cpp')

diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 9da8a194..e49fcf57 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -414,6 +414,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
                           return eventCopy;
                   },
                   ev);
+
                 events.remove(txn_id);
                 events.insert(event_id, ev);
 
@@ -666,7 +667,7 @@ TimelineModel::internalAddEvents(
   const std::vector &timeline)
 {
         std::vector ids;
-        for (const auto &e : timeline) {
+        for (auto e : timeline) {
                 QString id =
                   boost::apply_visitor([](const auto &e) -> QString { return eventId(e); }, e);
 
@@ -707,16 +708,16 @@ TimelineModel::internalAddEvents(
 
                 if (auto event =
                       boost::get>(&e)) {
-                        auto temp    = decryptEvent(*event).event;
-                        auto encInfo = boost::apply_visitor(
-                          [](const auto &ev) -> boost::optional {
-                                  return eventEncryptionInfo(ev);
-                          },
-                          temp);
-
-                        if (encInfo)
-                                emit newEncryptedImage(encInfo.value());
+                        e = decryptEvent(*event).event;
                 }
+                auto encInfo = boost::apply_visitor(
+                  [](const auto &ev) -> boost::optional {
+                          return eventEncryptionInfo(ev);
+                  },
+                  e);
+
+                if (encInfo)
+                        emit newEncryptedImage(encInfo.value());
 
                 this->events.insert(id, e);
                 ids.push_back(id);
-- 
cgit 1.5.1


From 659e36b113158ba9870e201dab7098888bf9d275 Mon Sep 17 00:00:00 2001
From: Nicolas Werner 
Date: Sat, 14 Dec 2019 17:08:36 +0100
Subject: Update to c++17

---
 .ci/install.sh                       |  33 +-------
 .ci/linux/deploy.sh                  |   2 +-
 .ci/script.sh                        |   8 --
 .travis.yml                          | 105 ++++++++++++++++-------
 CMakeLists.txt                       |  13 ++-
 deps/CMakeLists.txt                  |   4 +-
 deps/cmake/Json.cmake                |   2 +-
 src/Cache.cpp                        |  30 ++++---
 src/Cache.h                          |  85 ++++++++++---------
 src/ChatPage.cpp                     |  10 +--
 src/ChatPage.h                       |  12 +--
 src/Olm.cpp                          |   7 +-
 src/Utils.cpp                        |  67 +++++++--------
 src/Utils.h                          |  17 ++--
 src/timeline/TimelineModel.cpp       | 157 +++++++++++++++++------------------
 src/timeline/TimelineViewManager.cpp |   8 +-
 src/timeline/TimelineViewManager.h   |   8 +-
 17 files changed, 284 insertions(+), 284 deletions(-)

(limited to 'src/timeline/TimelineModel.cpp')

diff --git a/.ci/install.sh b/.ci/install.sh
index 57e73e03..776d4d23 100755
--- a/.ci/install.sh
+++ b/.ci/install.sh
@@ -3,14 +3,6 @@
 set -ex
 
 if [ "$TRAVIS_OS_NAME" = "osx" ]; then
-    brew update
-
-    # uninstall packages, that would get upgraded by upgrading cmake (and we don't need)
-    brew uninstall --force cgal node sfcgal postgis
-
-    brew install qt5 lmdb clang-format ninja libsodium cmark
-    brew upgrade boost cmake icu4c || true
-
     brew tap nlohmann/json
     brew install --with-cmake nlohmann_json
 
@@ -25,11 +17,11 @@ fi
 
 
 if [ "$TRAVIS_OS_NAME" = "linux" ]; then
+    sudo update-alternatives --install /usr/bin/gcc gcc "/usr/bin/${CC}" 10
+    sudo update-alternatives --install /usr/bin/g++ g++ "/usr/bin/${CXX}" 10
 
-    if [ -z "$QT_VERSION" ]; then
-        QT_VERSION="592"
-        QT_PKG="59"
-    fi
+    sudo update-alternatives --set gcc "/usr/bin/${CC}"
+    sudo update-alternatives --set g++ "/usr/bin/${CXX}"
 
     wget https://cmake.org/files/v3.15/cmake-3.15.5-Linux-x86_64.sh
     sudo sh cmake-3.15.5-Linux-x86_64.sh  --skip-license  --prefix=/usr/local
@@ -40,21 +32,4 @@ if [ "$TRAVIS_OS_NAME" = "linux" ]; then
       tar xfz libsodium-1.0.17.tar.gz
       cd libsodium-1.0.17/
       ./configure && make && sudo make install )
-
-    sudo add-apt-repository -y ppa:beineri/opt-qt${QT_VERSION}-trusty
-    # needed for git-lfs, otherwise the follow apt update fails.
-    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 6B05F25D762E3157
-
-    # needed for mongodb repository: https://github.com/travis-ci/travis-ci/issues/9037
-    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
-
-    sudo apt update -qq
-    sudo apt install -qq -y \
-        qt${QT_PKG}base \
-        qt${QT_PKG}tools \
-        qt${QT_PKG}svg \
-        qt${QT_PKG}multimedia \
-        qt${QT_PKG}quickcontrols2 \
-        qt${QT_PKG}graphicaleffects \
-        liblmdb-dev
 fi
diff --git a/.ci/linux/deploy.sh b/.ci/linux/deploy.sh
index 524d72d5..ff8ef6ca 100755
--- a/.ci/linux/deploy.sh
+++ b/.ci/linux/deploy.sh
@@ -36,7 +36,7 @@ unset LD_LIBRARY_PATH
 
 ARCH=$(uname -m)
 export ARCH
-LD_LIBRARY_PATH=$(pwd)/.deps/usr/lib/:$LD_LIBRARY_PATH
+LD_LIBRARY_PATH=$(pwd)/.deps/usr/lib/:/usr/local/lib/:$LD_LIBRARY_PATH
 export LD_LIBRARY_PATH
 
 for res in ./linuxdeployqt*.AppImage
diff --git a/.ci/script.sh b/.ci/script.sh
index 06536278..7e9a8d81 100755
--- a/.ci/script.sh
+++ b/.ci/script.sh
@@ -3,17 +3,9 @@
 set -ex
 
 if [ "$TRAVIS_OS_NAME" = "linux" ]; then
-    export CC=${C_COMPILER}
-    export CXX=${CXX_COMPILER}
     # make build use all available cores
     export CMAKE_BUILD_PARALLEL_LEVEL=$(cat /proc/cpuinfo | awk '/^processor/{print $3}' | wc -l)
 
-    sudo update-alternatives --install /usr/bin/gcc gcc "/usr/bin/${C_COMPILER}" 10
-    sudo update-alternatives --install /usr/bin/g++ g++ "/usr/bin/${CXX_COMPILER}" 10
-
-    sudo update-alternatives --set gcc "/usr/bin/${C_COMPILER}"
-    sudo update-alternatives --set g++ "/usr/bin/${CXX_COMPILER}"
-
     export PATH="/usr/local/bin/:${PATH}"
     cmake --version
 fi
diff --git a/.travis.yml b/.travis.yml
index 3abb8bfd..1e2bc5b8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: cpp
 sudo: required
-dist: trusty
+dist: xenial
 
 notifications:
   webhooks:
@@ -19,61 +19,108 @@ matrix:
     include:
         - os: osx
           compiler: clang
-          # Use the default osx image, because that one is actually tested to work with homebrew and probably the oldest supported version
-          #          osx_image: xcode9
+          # C++17 support
+          osx_image: xcode10.2
           env:
-              - DEPLOYMENT=1
-              - USE_BUNDLED_BOOST=0
-              - USE_BUNDLED_CMARK=0
-              - USE_BUNDLED_JSON=0
-              - MTX_STATIC=1
+            - DEPLOYMENT=1
+            - USE_BUNDLED_BOOST=0
+            - USE_BUNDLED_CMARK=0
+            - USE_BUNDLED_JSON=0
+            - MTX_STATIC=1
+          addons:
+            homebrew:
+              taps: nlohmann/json
+              packages:
+                - boost
+                - clang-format
+                - cmake
+                - cmark
+                - icu4c
+                - libsodium
+                - lmdb
+                - ninja
+                - openssl
+                - qt5
         - os: linux
-          compiler: gcc
+          compiler: gcc-7
           env:
-              - CXX_COMPILER=g++-5
-              - C_COMPILER=gcc-5
-              - QT_VERSION="-5.10.1"
-              - QT_PKG=510
+              - CXX=g++-7
+              - CC=gcc-7
+              - QT_PKG=512
               - DEPLOYMENT=1
               - USE_BUNDLED_BOOST=1
               - USE_BUNDLED_CMARK=1
               - USE_BUNDLED_JSON=1
           addons:
               apt:
-                  sources: ["ubuntu-toolchain-r-test"]
-                  packages: ["g++-5", "ninja-build"]
+                  sources: 
+                    - ubuntu-toolchain-r-test
+                    - sourceline: 'ppa:beineri/opt-qt-5.12.6-xenial'
+                  packages: 
+                    - g++-7 
+                    - ninja-build
+                    - qt512base
+                    - qt512tools
+                    - qt512svg
+                    - qt512multimedia
+                    - qt512quickcontrols2
+                    - qt512graphicaleffects
+                    - liblmdb-dev
+                    - libgl1-mesa-dev # needed for missing gl.h
         - os: linux
-          compiler: gcc
+          compiler: gcc-8
           env:
-              - CXX_COMPILER=g++-8
-              - C_COMPILER=gcc-8
-              - QT_VERSION=592
+              - CXX=g++-8
+              - CC=gcc-8
               - QT_PKG=59
               - USE_BUNDLED_BOOST=1
               - USE_BUNDLED_CMARK=1
               - USE_BUNDLED_JSON=1
           addons:
               apt:
-                  sources: ["ubuntu-toolchain-r-test"]
-                  packages: ["g++-8", "ninja-build"]
+                  sources: 
+                    - ubuntu-toolchain-r-test
+                    - sourceline: 'ppa:beineri/opt-qt597-xenial'
+                  packages: 
+                    - g++-8 
+                    - ninja-build
+                    - qt59base
+                    - qt59tools
+                    - qt59svg
+                    - qt59multimedia
+                    - qt59quickcontrols2
+                    - qt59graphicaleffects
+                    - liblmdb-dev
+                    - libgl1-mesa-dev # needed for missing gl.h
         - os: linux
-          compiler: clang
+          compiler: clang-6
           env:
-              - CXX_COMPILER=clang++-5.0
-              - C_COMPILER=clang-5.0
-              - QT_VERSION=592
+              - CXX=clang++-6.0
+              - CC=clang-6.0
               - QT_PKG=59
               - USE_BUNDLED_BOOST=1
               - USE_BUNDLED_CMARK=1
               - USE_BUNDLED_JSON=1
           addons:
               apt:
-                  sources: ["ubuntu-toolchain-r-test", "llvm-toolchain-trusty-5.0"]
-                  packages: ["clang-5.0", "g++-7", "ninja-build"]
+                  sources: 
+                    - ubuntu-toolchain-r-test
+                    - llvm-toolchain-xenial-6.0
+                    - sourceline: 'ppa:beineri/opt-qt597-xenial'
+                  packages: 
+                    - clang++-6.0
+                    - g++-7 
+                    - ninja-build
+                    - qt59base
+                    - qt59tools
+                    - qt59svg
+                    - qt59multimedia
+                    - qt59quickcontrols2
+                    - qt59graphicaleffects
+                    - liblmdb-dev
+                    - libgl1-mesa-dev # needed for missing gl.h
 
 before_install:
-    - export CXX=${CXX_COMPILER}
-    - export CC=${C_COMPILER}
     # Use TRAVIS_TAG if defined, or the short commit SHA otherwise
     - export VERSION=${TRAVIS_TAG:-$(git rev-parse --short HEAD)}
 install:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 67a1dfb0..39dadc64 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -5,9 +5,6 @@ option(ASAN "Compile with address sanitizers" OFF)
 
 set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
 
-add_definitions(-DBOOST_MPL_LIMIT_LIST_SIZE=30)
-add_definitions(-DBOOST_MPL_CFG_NO_PREPROCESSED_HEADERS)
-
 include(GNUInstallDirs)
 
 # Include Qt basic functions
@@ -15,8 +12,8 @@ include(QtCommon)
 
 project(nheko LANGUAGES C CXX)
 set(CPACK_PACKAGE_VERSION_MAJOR "0")
-set(CPACK_PACKAGE_VERSION_MINOR "6")
-set(CPACK_PACKAGE_VERSION_PATCH "4")
+set(CPACK_PACKAGE_VERSION_MINOR "7")
+set(CPACK_PACKAGE_VERSION_PATCH "0")
 set(PROJECT_VERSION_MAJOR ${CPACK_PACKAGE_VERSION_MAJOR})
 set(PROJECT_VERSION_MINOR ${CPACK_PACKAGE_VERSION_MINOR})
 set(PROJECT_VERSION_PATCH ${CPACK_PACKAGE_VERSION_PATCH})
@@ -27,7 +24,7 @@ fix_project_version()
 
 # Set additional project information
 set(COMPANY "Nheko")
-set(COPYRIGHT "Copyright (c) 2018 Nheko Contributors")
+set(COPYRIGHT "Copyright (c) 2019 Nheko Contributors")
 set(IDENTIFIER "com.github.mujx.nheko")
 
 add_project_meta(META_FILES_TO_INCLUDE)
@@ -91,7 +88,7 @@ if (NOT MSVC)
     set(CMAKE_C_COMPILER gcc)
 endif(NOT MSVC)
 
-set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED ON)
 set(CMAKE_INCLUDE_CURRENT_DIR ON)
 if(NOT MSVC)
@@ -106,7 +103,7 @@ if(NOT MSVC)
         -fsized-deallocation \
         -fdiagnostics-color=always \
         -Wunreachable-code \
-	-std=c++14"
+	-std=c++17"
     )
     if (NOT CMAKE_COMPILER_IS_GNUCXX)
         # -Wshadow is buggy and broken in GCC, so do not enable it.
diff --git a/deps/CMakeLists.txt b/deps/CMakeLists.txt
index fbe9275f..cf42b7b5 100644
--- a/deps/CMakeLists.txt
+++ b/deps/CMakeLists.txt
@@ -46,10 +46,10 @@ set(BOOST_SHA256
 
 set(
   MTXCLIENT_URL
-  https://github.com/Nheko-Reborn/mtxclient/archive/1945952864ef87a6afeb57b0beb80756d0647381.zip
+  https://github.com/Nheko-Reborn/mtxclient/archive/fd5208d85ecc9e077fe0fde5424ba225849cccf7.zip
   )
 set(MTXCLIENT_HASH
-    727cd145c0c1e9c168aaeded3b7cc9801c63b4da5e8cd0a4782982d08770816e)
+    24ef9d5562ed6d83d6c28f6c8de559487029f3bdc35d9fe8e5799283ff999513)
 set(
   TWEENY_URL
   https://github.com/mobius3/tweeny/archive/b94ce07cfb02a0eb8ac8aaf66137dabdaea857cf.tar.gz
diff --git a/deps/cmake/Json.cmake b/deps/cmake/Json.cmake
index 3b63550e..c5e66cea 100644
--- a/deps/cmake/Json.cmake
+++ b/deps/cmake/Json.cmake
@@ -13,7 +13,7 @@ ExternalProject_Add(
         -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}
 
   BUILD_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/json 
-  INSTALL_COMMAND make install
+  INSTALL_COMMAND ${CMAKE_COMMAND} --build ${DEPS_BUILD_DIR}/json --target install
 )
 
 list(APPEND THIRD_PARTY_DEPS Json)
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 083dbe89..d3aec9db 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -17,6 +17,7 @@
 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -26,7 +27,6 @@
 #include 
 #include 
 
-#include 
 #include 
 
 #include "Cache.h"
@@ -395,7 +395,7 @@ Cache::saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr
         txn.commit();
 }
 
-boost::optional
+std::optional
 Cache::getOlmSession(const std::string &curve25519, const std::string &session_id)
 {
         using namespace mtx::crypto;
@@ -413,7 +413,7 @@ Cache::getOlmSession(const std::string &curve25519, const std::string &session_i
                 return unpickle(data, SECRET);
         }
 
-        return boost::none;
+        return std::nullopt;
 }
 
 std::vector
@@ -967,8 +967,8 @@ Cache::saveState(const mtx::responses::Sync &res)
                 bool has_new_tags = false;
                 for (const auto &evt : room.second.account_data.events) {
                         // for now only fetch tag events
-                        if (evt.type() == typeid(Event)) {
-                                auto tags_evt = boost::get>(evt);
+                        if (std::holds_alternative>(evt)) {
+                                auto tags_evt = std::get>(evt);
                                 has_new_tags  = true;
                                 for (const auto &tag : tags_evt.content.tags) {
                                         updatedInfo.tags.push_back(tag.first);
@@ -1049,19 +1049,17 @@ Cache::saveInvite(lmdb::txn &txn,
         using namespace mtx::events::state;
 
         for (const auto &e : room.invite_state) {
-                if (boost::get>(&e) != nullptr) {
-                        auto msg = boost::get>(e);
+                if (auto msg = std::get_if>(&e)) {
+                        auto display_name = msg->content.display_name.empty()
+                                              ? msg->state_key
+                                              : msg->content.display_name;
 
-                        auto display_name = msg.content.display_name.empty()
-                                              ? msg.state_key
-                                              : msg.content.display_name;
-
-                        MemberInfo tmp{display_name, msg.content.avatar_url};
+                        MemberInfo tmp{display_name, msg->content.avatar_url};
 
                         lmdb::dbi_put(
-                          txn, membersdb, lmdb::val(msg.state_key), lmdb::val(json(tmp).dump()));
+                          txn, membersdb, lmdb::val(msg->state_key), lmdb::val(json(tmp).dump()));
                 } else {
-                        boost::apply_visitor(
+                        std::visit(
                           [&txn, &statesdb](auto msg) {
                                   bool res = lmdb::dbi_put(txn,
                                                            statesdb,
@@ -1122,7 +1120,7 @@ Cache::roomsWithTagUpdates(const mtx::responses::Sync &res)
         for (const auto &room : res.rooms.join) {
                 bool hasUpdates = false;
                 for (const auto &evt : room.second.account_data.events) {
-                        if (evt.type() == typeid(Event)) {
+                        if (std::holds_alternative>(evt)) {
                                 hasUpdates = true;
                         }
                 }
@@ -1940,7 +1938,7 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
                 if (isStateEvent(e))
                         continue;
 
-                if (boost::get>(&e) != nullptr)
+                if (std::holds_alternative>(e))
                         continue;
 
                 json obj = json::object();
diff --git a/src/Cache.h b/src/Cache.h
index f5e1cfa0..878ac9ce 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -17,7 +17,8 @@
 
 #pragma once
 
-#include 
+#include 
+#include 
 
 #include 
 #include 
@@ -25,11 +26,11 @@
 #include 
 
 #include 
+#include 
+
 #include 
 #include 
 #include 
-#include 
-#include 
 
 #include "Logging.h"
 #include "MatrixClient.h"
@@ -453,8 +454,8 @@ public:
         //
         void saveOlmSession(const std::string &curve25519, mtx::crypto::OlmSessionPtr session);
         std::vector getOlmSessions(const std::string &curve25519);
-        boost::optional getOlmSession(const std::string &curve25519,
-                                                                  const std::string &session_id);
+        std::optional getOlmSession(const std::string &curve25519,
+                                                                const std::string &session_id);
 
         void saveOlmAccount(const std::string &pickled);
         std::string restoreOlmAccount();
@@ -517,52 +518,50 @@ private:
                 using namespace mtx::events;
                 using namespace mtx::events::state;
 
-                if (boost::get>(&event) != nullptr) {
-                        const auto e = boost::get>(event);
-
-                        switch (e.content.membership) {
+                if (auto e = std::get_if>(&event); e != nullptr) {
+                        switch (e->content.membership) {
                         //
                         // We only keep users with invite or join membership.
                         //
                         case Membership::Invite:
                         case Membership::Join: {
-                                auto display_name = e.content.display_name.empty()
-                                                      ? e.state_key
-                                                      : e.content.display_name;
+                                auto display_name = e->content.display_name.empty()
+                                                      ? e->state_key
+                                                      : e->content.display_name;
 
                                 // Lightweight representation of a member.
-                                MemberInfo tmp{display_name, e.content.avatar_url};
+                                MemberInfo tmp{display_name, e->content.avatar_url};
 
                                 lmdb::dbi_put(txn,
                                               membersdb,
-                                              lmdb::val(e.state_key),
+                                              lmdb::val(e->state_key),
                                               lmdb::val(json(tmp).dump()));
 
                                 insertDisplayName(QString::fromStdString(room_id),
-                                                  QString::fromStdString(e.state_key),
+                                                  QString::fromStdString(e->state_key),
                                                   QString::fromStdString(display_name));
 
                                 insertAvatarUrl(QString::fromStdString(room_id),
-                                                QString::fromStdString(e.state_key),
-                                                QString::fromStdString(e.content.avatar_url));
+                                                QString::fromStdString(e->state_key),
+                                                QString::fromStdString(e->content.avatar_url));
 
                                 break;
                         }
                         default: {
                                 lmdb::dbi_del(
-                                  txn, membersdb, lmdb::val(e.state_key), lmdb::val(""));
+                                  txn, membersdb, lmdb::val(e->state_key), lmdb::val(""));
 
                                 removeDisplayName(QString::fromStdString(room_id),
-                                                  QString::fromStdString(e.state_key));
+                                                  QString::fromStdString(e->state_key));
                                 removeAvatarUrl(QString::fromStdString(room_id),
-                                                QString::fromStdString(e.state_key));
+                                                QString::fromStdString(e->state_key));
 
                                 break;
                         }
                         }
 
                         return;
-                } else if (boost::get>(&event) != nullptr) {
+                } else if (std::holds_alternative>(event)) {
                         setEncryptedRoom(txn, room_id);
                         return;
                 }
@@ -570,7 +569,7 @@ private:
                 if (!isStateEvent(event))
                         return;
 
-                boost::apply_visitor(
+                std::visit(
                   [&txn, &statesdb](auto e) {
                           lmdb::dbi_put(
                             txn, statesdb, lmdb::val(to_string(e.type)), lmdb::val(json(e).dump()));
@@ -584,17 +583,17 @@ private:
                 using namespace mtx::events;
                 using namespace mtx::events::state;
 
-                return boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr;
+                return std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e);
         }
 
         template
@@ -603,11 +602,11 @@ private:
                 using namespace mtx::events;
                 using namespace mtx::events::state;
 
-                return boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr;
+                return std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e);
         }
 
         bool containsStateUpdates(const mtx::events::collections::StrippedEvents &e)
@@ -615,11 +614,11 @@ private:
                 using namespace mtx::events;
                 using namespace mtx::events::state;
 
-                return boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr ||
-                       boost::get>(&e) != nullptr;
+                return std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e) ||
+                       std::holds_alternative>(e);
         }
 
         void saveInvites(lmdb::txn &txn,
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 35d262ac..c496acab 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -54,7 +54,7 @@ constexpr int CHECK_CONNECTIVITY_INTERVAL = 15'000;
 constexpr int RETRY_TIMEOUT               = 5'000;
 constexpr size_t MAX_ONETIME_KEYS         = 50;
 
-Q_DECLARE_METATYPE(boost::optional)
+Q_DECLARE_METATYPE(std::optional)
 
 ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
   : QWidget(parent)
@@ -64,8 +64,8 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
 {
         setObjectName("chatPage");
 
-        qRegisterMetaType>(
-          "boost::optional");
+        qRegisterMetaType>(
+          "std::optional");
 
         topLayout_ = new QHBoxLayout(this);
         topLayout_->setSpacing(0);
@@ -318,7 +318,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
 
                   auto bin     = dev->peek(dev->size());
                   auto payload = std::string(bin.data(), bin.size());
-                  boost::optional encryptedFile;
+                  std::optional encryptedFile;
                   if (cache::client()->isRoomEncrypted(current_room_.toStdString())) {
                           mtx::crypto::BinaryBuf buf;
                           std::tie(buf, encryptedFile) = mtx::crypto::encrypt_file(payload);
@@ -371,7 +371,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 this,
                 [this](QString roomid,
                        QString filename,
-                       boost::optional encryptedFile,
+                       std::optional encryptedFile,
                        QString url,
                        QString mimeClass,
                        QString mime,
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 20e156af..6ca30b3d 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -18,8 +18,9 @@
 #pragma once
 
 #include 
-#include 
-#include 
+#include 
+#include 
+
 #include 
 #include 
 
@@ -98,7 +99,7 @@ signals:
         void uploadFailed(const QString &msg);
         void mediaUploaded(const QString &roomid,
                            const QString &filename,
-                           const boost::optional &file,
+                           const std::optional &file,
                            const QString &url,
                            const QString &mimeClass,
                            const QString &mime,
@@ -252,9 +253,8 @@ ChatPage::getMemberships(const std::vector &collection) const
         using Member = mtx::events::StateEvent;
 
         for (const auto &event : collection) {
-                if (boost::get(event) != nullptr) {
-                        auto member = boost::get(event);
-                        memberships.emplace(member.state_key, member);
+                if (auto member = std::get_if(event)) {
+                        memberships.emplace(member->state_key, *member);
                 }
         }
 
diff --git a/src/Olm.cpp b/src/Olm.cpp
index c1598570..9c1a25df 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -1,4 +1,4 @@
-#include 
+#include 
 
 #include "Olm.h"
 
@@ -289,14 +289,13 @@ request_keys(const std::string &room_id, const std::string &event_id)
                           return;
                   }
 
-                  if (boost::get>(&res) == nullptr) {
+                  if (!std::holds_alternative>(res)) {
                           nhlog::net()->info(
                             "retrieved event is not encrypted: {} from {}", event_id, room_id);
                           return;
                   }
 
-                  olm::send_key_request_for(room_id,
-                                            boost::get>(res));
+                  olm::send_key_request_for(room_id, std::get>(res));
           });
 }
 
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 8f9e0643..3d69162f 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -9,9 +9,10 @@
 #include 
 #include 
 #include 
+
 #include 
+#include 
 
-#include 
 #include 
 
 #include "Config.h"
@@ -122,34 +123,33 @@ utils::getMessageDescription(const TimelineEvent &event,
         using Video     = mtx::events::RoomEvent;
         using Encrypted = mtx::events::EncryptedEvent;
 
-        if (boost::get