Move location models to LibNeoChat

This commit is contained in:
James Graham
2025-04-13 15:52:01 +01:00
parent 866fee2ea3
commit 4fe9c76d90
6 changed files with 2 additions and 4 deletions

View File

@@ -0,0 +1,181 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "livelocationsmodel.h"
#include <Quotient/events/roommessageevent.h>
#include <QDebug>
#include <cmath>
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
connect(this, &LiveLocationsModel::dataChanged, this, &LiveLocationsModel::boundingBoxChanged);
connect(this, &LiveLocationsModel::rowsInserted, this, &LiveLocationsModel::boundingBoxChanged);
}
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"_L1].toObject()["uri"_L1].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"_L1].toObject()["uri"_L1].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"_L1].toObject()["type"_L1].toString();
case AuthorRole:
return QVariant::fromValue(m_room->member(data.senderId));
case IsLiveRole: {
if (!data.beaconInfo["live"_L1].toBool()) {
return false;
}
// TODO Qt6: port to toInteger(), timestamps are in ms since epoch, ie. 64 bit values
const auto lastTs = std::max(data.beaconInfo.value("org.matrix.msc3488.ts"_L1).toDouble(), data.beacon.value("org.matrix.msc3488.ts"_L1).toDouble());
const auto timeout = data.beaconInfo.value("timeout"_L1).toDouble(600000);
return lastTs + timeout >= QDateTime::currentDateTime().toMSecsSinceEpoch();
}
case HeadingRole: {
bool success = false;
const auto heading = data.beacon["org.matrix.msc3488.location"_L1].toObject()["org.kde.itinerary.heading"_L1].toString().toDouble(&success);
return success ? heading : NAN;
}
}
return {};
}
QHash<int, QByteArray> 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");
r.insert(HeadingRole, "heading");
return r;
}
QRectF LiveLocationsModel::boundingBox() const
{
QRectF bbox(QPointF(180.0, 90.0), QPointF(-180.0, -90.0));
for (auto i = 0; i < rowCount(); ++i) {
const auto lat = data(index(i, 0), LatitudeRole).toDouble();
const auto lon = data(index(i, 0), LongitudeRole).toDouble();
bbox.setLeft(std::min(bbox.left(), lon));
bbox.setRight(std::max(bbox.right(), lon));
bbox.setTop(std::min(bbox.top(), lat));
bbox.setBottom(std::max(bbox.bottom(), lat));
}
return bbox;
}
void LiveLocationsModel::addEvent(const Quotient::RoomEvent *event)
{
if (event->isStateEvent() && event->matrixType() == "org.matrix.msc3672.beacon_info"_L1) {
LiveLocationData data;
data.senderId = event->senderId();
data.beaconInfo = event->contentJson();
if (event->contentJson()["live"_L1].toBool()) {
data.eventId = event->id();
} else {
data.eventId = event->fullJson()["replaces_state"_L1].toString();
}
updateLocationData(std::move(data));
}
if (event->matrixType() == "org.matrix.msc3672.beacon"_L1) {
LiveLocationData data;
data.eventId = event->contentJson()["m.relates_to"_L1].toObject()["event_id"_L1].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"_L1).toDouble() < data.beacon.value("org.matrix.msc3488.ts"_L1).toDouble()) {
it->beacon = std::move(data.beacon);
}
if (it->beaconInfo.isEmpty()
|| it->beaconInfo.value("org.matrix.msc3488.ts"_L1).toDouble() < data.beaconInfo.value("org.matrix.msc3488.ts"_L1).toDouble()) {
it->beaconInfo = std::move(data.beaconInfo);
}
Q_EMIT dataChanged(idx, idx);
}
#include "moc_livelocationsmodel.cpp"

View File

@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-FileCopyrightText: 2023 Volker Krause <vkrause@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "neochatroom.h"
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QRectF>
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
QML_ELEMENT
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)
/** Bounding box of all live location beacons covered by this model. */
Q_PROPERTY(QRectF boundingBox READ boundingBox NOTIFY boundingBoxChanged)
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. */
HeadingRole, /**< Heading in degree (not part of any MSC yet, using an Itinerary extension). */
};
Q_ENUM(Roles)
int rowCount(const QModelIndex &parent = {}) const override;
QVariant data(const QModelIndex &index, int roleName) const override;
QHash<int, QByteArray> roleNames() const override;
QRectF boundingBox() const;
Q_SIGNALS:
void roomChanged();
void eventIdChanged();
void boundingBoxChanged();
private:
void addEvent(const Quotient::RoomEvent *event);
void updateLocationData(LiveLocationData &&data);
QPointer<NeoChatRoom> m_room;
QString m_eventId;
QList<LiveLocationData> m_locations;
};

View File

@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "locationsmodel.h"
#include <QGuiApplication>
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &LocationsModel::roomChanged, this, [this]() {
for (const auto &event : m_room->messageEvents()) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [this](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"_L1] == "m.location"_L1) {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
});
connect(this, &LocationsModel::rowsInserted, this, &LocationsModel::boundingBoxChanged);
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"_L1]["uri"_L1].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
if (parts.size() < 2) {
qWarning() << "invalid geo: URI" << uri;
return;
}
const auto latitude = parts[0].toFloat();
const auto longitude = parts[1].toFloat();
beginInsertRows(QModelIndex(), m_locations.size(), m_locations.size() + 1);
m_locations += LocationData{
.eventId = event->id(),
.latitude = latitude,
.longitude = longitude,
.content = event->contentJson(),
.member = m_room->member(event->senderId()),
};
endInsertRows();
}
NeoChatRoom *LocationsModel::room() const
{
return m_room;
}
void LocationsModel::setRoom(NeoChatRoom *room)
{
if (m_room) {
disconnect(this, nullptr, m_room, nullptr);
}
m_room = room;
Q_EMIT roomChanged();
}
QHash<int, QByteArray> LocationsModel::roleNames() const
{
return {
{LongitudeRole, "longitude"},
{LatitudeRole, "latitude"},
{TextRole, "text"},
{AssetRole, "asset"},
{AuthorRole, "author"},
};
}
QVariant LocationsModel::data(const QModelIndex &index, int roleName) const
{
auto row = index.row();
if (roleName == LongitudeRole) {
return m_locations[row].longitude;
} else if (roleName == LatitudeRole) {
return m_locations[row].latitude;
} else if (roleName == TextRole) {
return m_locations[row].content["body"_L1].toString();
} else if (roleName == AssetRole) {
return m_locations[row].content["org.matrix.msc3488.asset"_L1].toObject()["type"_L1].toString();
} else if (roleName == AuthorRole) {
return QVariant::fromValue(m_locations[row].member);
}
return {};
}
int LocationsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_locations.size();
}
QRectF LocationsModel::boundingBox() const
{
QRectF bbox(QPointF(180.0, 90.0), QPointF(-180.0, -90.0));
for (auto i = 0; i < rowCount(); ++i) {
const auto lat = data(index(i, 0), LatitudeRole).toDouble();
const auto lon = data(index(i, 0), LongitudeRole).toDouble();
bbox.setLeft(std::min(bbox.left(), lon));
bbox.setRight(std::max(bbox.right(), lon));
bbox.setTop(std::min(bbox.top(), lat));
bbox.setBottom(std::max(bbox.bottom(), lat));
}
return bbox;
}
bool LocationsModel::event(QEvent *event)
{
if (event->type() == QEvent::ApplicationPaletteChange) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
return QObject::event(event);
}
#include "moc_locationsmodel.cpp"

View File

@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QPointer>
#include <QQmlEngine>
#include <QRectF>
#include "neochatroom.h"
#include <Quotient/events/roommessageevent.h>
#include <Quotient/roommember.h>
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
QML_ELEMENT
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
AssetRole,
AuthorRole,
};
Q_ENUM(Roles)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
/** Bounding box of all locations covered by this model. */
Q_PROPERTY(QRectF boundingBox READ boundingBox NOTIFY boundingBoxChanged)
explicit LocationsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
QRectF boundingBox() const;
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent = {}) const override;
Q_SIGNALS:
void roomChanged();
void boundingBoxChanged();
protected:
bool event(QEvent *event) override;
private:
QPointer<NeoChatRoom> m_room;
struct LocationData {
QString eventId;
float latitude;
float longitude;
QJsonObject content;
Quotient::RoomMember member;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);
};