diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b11edb092..38aab75f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,7 @@ add_library(neochat STATIC events/stickerevent.cpp models/reactionmodel.cpp delegatesizehelper.cpp + models/livelocationsmodel.cpp ) ecm_qt_declare_logging_category(neochat diff --git a/src/main.cpp b/src/main.cpp index c95ffabb7..23dbcbb72 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -57,6 +57,7 @@ #include "models/emoticonfiltermodel.h" #include "models/imagepacksmodel.h" #include "models/keywordnotificationrulemodel.h" +#include "models/livelocationsmodel.h" #include "models/messageeventmodel.h" #include "models/messagefiltermodel.h" #include "models/publicroomlistmodel.h" @@ -247,6 +248,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "StateModel"); qmlRegisterType("org.kde.neochat", 1, 0, "StateFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SearchModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "LiveLocationsModel"); #ifdef QUOTIENT_07 qmlRegisterType("org.kde.neochat", 1, 0, "PollHandler"); #endif diff --git a/src/models/livelocationsmodel.cpp b/src/models/livelocationsmodel.cpp new file mode 100644 index 000000000..61e1587b7 --- /dev/null +++ b/src/models/livelocationsmodel.cpp @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-FileCopyrightText: 2023 Volker Krause +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "livelocationsmodel.h" + +#include + +#include + +using namespace Quotient; + +bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs) +{ + return lhs.eventId < rhs.eventId; +} + +LiveLocationsModel::LiveLocationsModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect( + this, + &LiveLocationsModel::roomChanged, + this, + [this]() { + for (const auto &event : m_room->messageEvents()) { + addEvent(event.get()); + } + connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [this](const auto &events) { + for (const auto &event : events) { + addEvent(event.get()); + } + }); + connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) { + for (const auto &event : events) { + addEvent(event.get()); + } + }); + }, + Qt::QueuedConnection); // deferred so we are sure the eventId filter is set +} + +int LiveLocationsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_locations.size(); +} + +QVariant LiveLocationsModel::data(const QModelIndex &index, int roleName) const +{ + if (!checkIndex(index)) { + return {}; + } + + const auto &data = m_locations.at(index.row()); + switch (roleName) { + case LatitudeRole: { + const auto geoUri = data.beacon["org.matrix.msc3488.location"_ls].toObject()["uri"_ls].toString(); + if (geoUri.isEmpty()) { + return {}; + } + const auto latitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[0]; + return latitude.toFloat(); + } + case LongitudeRole: { + const auto geoUri = data.beacon["org.matrix.msc3488.location"_ls].toObject()["uri"_ls].toString(); + if (geoUri.isEmpty()) { + return {}; + } + const auto longitude = geoUri.split(u';')[0].split(u':')[1].split(u',')[1]; + return longitude.toFloat(); + } + case AssetRole: + return data.beaconInfo["org.matrix.msc3488.asset"_ls].toObject()["type"].toString(); + case AuthorRole: + return m_room->getUser(data.senderId); + case IsLiveRole: + return data.beaconInfo["live"_ls].toBool(); + } + + return {}; +} + +QHash LiveLocationsModel::roleNames() const +{ + auto r = QAbstractListModel::roleNames(); + r.insert(LatitudeRole, "latitude"); + r.insert(LongitudeRole, "longitude"); + r.insert(AssetRole, "asset"); + r.insert(AuthorRole, "author"); + r.insert(IsLiveRole, "isLive"); + return r; +} + +void LiveLocationsModel::addEvent(const Quotient::RoomEvent *event) +{ + if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info") { + LiveLocationData data; + data.senderId = event->senderId(); + data.beaconInfo = event->contentJson(); + if (event->contentJson()["live"_ls].toBool()) { + data.eventId = event->id(); + } else { + data.eventId = event->fullJson()["replaces_state"_ls].toString(); + } + updateLocationData(std::move(data)); + } + if (event->matrixType() == "org.matrix.msc3672.beacon"_ls) { + LiveLocationData data; + data.eventId = event->contentJson()["m.relates_to"_ls].toObject()["event_id"_ls].toString(); + data.senderId = event->senderId(); + data.beacon = event->contentJson(); + updateLocationData(std::move(data)); + } +} + +void LiveLocationsModel::updateLocationData(LiveLocationData &&data) +{ + if (!m_eventId.isEmpty() && data.eventId != m_eventId) { + return; + } + + auto it = std::lower_bound(m_locations.begin(), m_locations.end(), data); + if (it == m_locations.end() || it->eventId != data.eventId) { + const auto row = std::distance(m_locations.begin(), it); + beginInsertRows({}, row, row); + m_locations.insert(it, std::move(data)); + endInsertRows(); + return; + } + + const auto idx = index(std::distance(m_locations.begin(), it), 0); + + // TODO Qt6: port to toInteger(), timestamps are in ms since epoch, ie. 64 bit values + if (it->beacon.isEmpty() || it->beacon.value("org.matrix.msc3488.ts"_ls).toDouble() < data.beacon.value("org.matrix.msc3488.ts"_ls).toDouble()) { + it->beacon = std::move(data.beacon); + } + if (it->beaconInfo.isEmpty() + || it->beaconInfo.value("org.matrix.msc3488.ts"_ls).toDouble() < data.beaconInfo.value("org.matrix.msc3488.ts"_ls).toDouble()) { + it->beaconInfo = std::move(data.beaconInfo); + } + + Q_EMIT dataChanged(idx, idx); +} diff --git a/src/models/livelocationsmodel.h b/src/models/livelocationsmodel.h new file mode 100644 index 000000000..979762779 --- /dev/null +++ b/src/models/livelocationsmodel.h @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-FileCopyrightText: 2023 Volker Krause +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "neochatroom.h" + +#include +#include + +namespace Quotient +{ +class RoomMessageEvent; +} + +struct LiveLocationData { + QString eventId; + QString senderId; + QJsonObject beaconInfo; + QJsonObject beacon; +}; +bool operator<(const LiveLocationData &lhs, const LiveLocationData &rhs); + +/** Accumulates live location beacon events in a given room + * and provides the last known state for one or more live location beacons. + */ +class LiveLocationsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(NeoChatRoom *room MEMBER m_room NOTIFY roomChanged) + /** The event id of the beacon start event, ie. the one all suspequent + * events use to relate to the same beacon. + * If this is set only this specific beacon will be coverd by this model, + * if it is empty, all beacons in the room will be covered. + */ + Q_PROPERTY(QString eventId MEMBER m_eventId NOTIFY eventIdChanged) + +public: + explicit LiveLocationsModel(QObject *parent = nullptr); + + enum Roles { + LatitudeRole, /**< Latest latitude of a live locaction beacon. */ + LongitudeRole, /**< Latest longitude of a live locaction beacon. */ + AssetRole, /**< Type of location event, e.g. self pin of the user location. */ + AuthorRole, /**< The author of the event. */ + IsLiveRole, /**< Boolean that indicates whether a live location beacon is still live. */ + }; + Q_ENUM(Roles) + + int rowCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int roleName) const override; + QHash roleNames() const override; + +Q_SIGNALS: + void roomChanged(); + void eventIdChanged(); + +private: + void addEvent(const Quotient::RoomEvent *event); + void updateLocationData(LiveLocationData &&data); + + QPointer m_room; + QString m_eventId; + + QList m_locations; +};