Move location models to LibNeoChat
This commit is contained in:
@@ -31,6 +31,8 @@ target_sources(LibNeoChat PRIVATE
|
||||
models/customemojimodel.cpp
|
||||
models/emojimodel.cpp
|
||||
models/imagepacksmodel.cpp
|
||||
models/livelocationsmodel.cpp
|
||||
models/locationsmodel.cpp
|
||||
models/stickermodel.cpp
|
||||
)
|
||||
|
||||
|
||||
181
src/libneochat/models/livelocationsmodel.cpp
Normal file
181
src/libneochat/models/livelocationsmodel.cpp
Normal 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"
|
||||
73
src/libneochat/models/livelocationsmodel.h
Normal file
73
src/libneochat/models/livelocationsmodel.h
Normal 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;
|
||||
};
|
||||
142
src/libneochat/models/locationsmodel.cpp
Normal file
142
src/libneochat/models/locationsmodel.cpp
Normal 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"
|
||||
64
src/libneochat/models/locationsmodel.h
Normal file
64
src/libneochat/models/locationsmodel.h
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user