Compare commits

...

2 Commits

Author SHA1 Message Date
Tobias Fella
73869e7b49 Use KDE's map config 2023-03-23 21:14:21 +01:00
Tobias Fella
072f7c6626 Show a map for location events 2023-03-23 21:14:20 +01:00
12 changed files with 391 additions and 0 deletions

View File

@@ -47,6 +47,7 @@ add_library(neochat STATIC
filetransferpseudojob.cpp
models/searchmodel.cpp
texthandler.cpp
models/locationsmodel.cpp
)
add_executable(neochat-app

View File

@@ -52,6 +52,7 @@
#include "models/devicesmodel.h"
#include "models/emojimodel.h"
#include "models/keywordnotificationrulemodel.h"
#include "models/locationsmodel.h"
#include "models/messageeventmodel.h"
#include "models/messagefiltermodel.h"
#include "models/publicroomlistmodel.h"
@@ -235,6 +236,7 @@ int main(int argc, char *argv[])
qmlRegisterType<CompletionModel>("org.kde.neochat", 1, 0, "CompletionModel");
qmlRegisterType<StateModel>("org.kde.neochat", 1, 0, "StateModel");
qmlRegisterType<SearchModel>("org.kde.neochat", 1, 0, "SearchModel");
qmlRegisterType<LocationsModel>("org.kde.neochat", 1, 0, "LocationsModel");
#ifdef QUOTIENT_07
qmlRegisterType<PollHandler>("org.kde.neochat", 1, 0, "PollHandler");
#endif

View File

@@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "locationsmodel.h"
using namespace Quotient;
LocationsModel::LocationsModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(this, &LocationsModel::roomChanged, this, [=]() {
for (const auto &event : m_room->messageEvents()) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) {
for (const auto &event : events) {
if (!is<RoomMessageEvent>(*event)) {
continue;
}
if (event->contentJson()["msgtype"] == "m.location") {
const auto &e = *event;
addLocation(eventCast<const RoomMessageEvent>(&e));
}
}
});
});
}
void LocationsModel::addLocation(const RoomMessageEvent *event)
{
const auto uri = event->contentJson()["org.matrix.msc3488.location"]["uri"].toString();
const auto parts = uri.mid(4).split(QLatin1Char(','));
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,
.text = event->contentJson()["body"].toString(),
.author = dynamic_cast<NeoChatUser *>(m_room->user(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"},
};
}
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].text;
}
return {};
}
int LocationsModel::rowCount(const QModelIndex &parent) const
{
return m_locations.size();
}

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QAbstractListModel>
#include "neochatroom.h"
#include <events/roommessageevent.h>
class LocationsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TextRole = Qt::DisplayRole,
LongitudeRole,
LatitudeRole,
};
Q_ENUM(Roles)
Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged)
explicit LocationsModel(QObject *parent = nullptr);
[[nodiscard]] NeoChatRoom *room() const;
void setRoom(NeoChatRoom *room);
[[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();
private:
QPointer<NeoChatRoom> m_room;
struct LocationData {
QString eventId;
float latitude;
float longitude;
QString text;
NeoChatUser *author;
};
QList<LocationData> m_locations;
void addLocation(const Quotient::RoomMessageEvent *event);
};

View File

@@ -510,6 +510,8 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
return DelegateType::Audio;
case MessageEventType::Video:
return DelegateType::Video;
case MessageEventType::Location:
return DelegateType::Location;
default:
break;
}
@@ -564,6 +566,9 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
}
if (auto e = eventCast<const RoomMessageEvent>(&evt)) {
if(e->msgtype() == Quotient::MessageEventType::Location) {
return e->contentJson();
}
// Cannot use e.contentJson() here because some
// EventContent classes inject values into the copy of the
// content JSON stored in EventContent::Base

View File

@@ -26,6 +26,7 @@ public:
Encrypted,
ReadMarker,
Poll,
Location,
Other,
};
Q_ENUM(DelegateType);

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow {
id: root
required property var content
flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground
visibility: Qt.WindowFullScreen
title: i18n("View Location")
Shortcut {
sequence: "Escape"
onActivated: root.destroy()
}
color: Kirigami.Theme.backgroundColor
background: AbstractButton {
onClicked: root.destroy()
}
Map {
id: map
anchors.fill: parent
property string latlong: root.content.geo_uri.split(':')[1]
property string latitude: latlong.split(',')[0]
property string longitude: latlong.split(',')[1]
center: QtPositioning.coordinate(latitude, longitude)
zoomLevel: 15
plugin: Plugin {
name: "osm"
PluginParameter {
name: "osm.useragent"
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
}
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: "https://autoconfig.kde.org/qtlocation/"
}
}
MapCircle {
radius: 1500 / map.zoomLevel
color: Kirigami.Theme.highlightColor
border.color: Kirigami.Theme.linkColor
border.width: Kirigami.Units.devicePixelRatio * 2
smooth: true
opacity: 0.25
center: QtPositioning.coordinate(map.latitude, map.longitude)
}
onCopyrightLinkActivated: {
Qt.openUrlExternally(link)
}
}
Button {
anchors.top: parent.top
anchors.right: parent.right
text: i18n("Close")
icon.name: "dialog-close"
display: AbstractButton.IconOnly
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2
onClicked: root.destroy()
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15 as QQC2
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.20 as Kirigami
import org.kde.neochat 1.0
Kirigami.Page {
id: locationsPage
required property var room
title: i18nc("Locations on a map", "Locations")
padding: 0
Map {
id: map
anchors.fill: parent
plugin: Plugin {
name: "osm"
PluginParameter {
name: "osm.useragent"
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
}
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: "https://autoconfig.kde.org/qtlocation/"
}
}
MapItemView {
model: LocationsModel {
room: locationsPage.room
}
delegate: MapQuickItem {
id: point
required property var longitude
required property var latitude
required property string text
anchorPoint.x: icon.width / 2
anchorPoint.y: icon.height / 2
coordinate: QtPositioning.coordinate(point.latitude, point.longitude)
autoFadeIn: false
sourceItem: Kirigami.Icon {
id: icon
width: height
height: Kirigami.Units.iconSizes.medium
source: "flag-blue"
}
}
}
}
}

View File

@@ -75,6 +75,11 @@ DelegateChooser {
delegate: PollDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Location
delegate: LocationDelegate {}
}
DelegateChoice {
roleValue: MessageEventModel.Other
delegate: Item {}

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
// SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtLocation 5.15
import QtPositioning 5.15
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0
TimelineContainer {
id: locationDelegate
property string latlong: model.content.geo_uri.split(':')[1]
property string latitude: latlong.split(',')[0]
property string longitude: latlong.split(',')[1]
property string formattedBody: model.content.formatted_body
ColumnLayout {
Layout.maximumWidth: locationDelegate.contentMaxWidth
Layout.preferredWidth: locationDelegate.contentMaxWidth
Map {
id: map
Layout.fillWidth: true
Layout.preferredHeight: locationDelegate.contentMaxWidth / 16 * 9
center: QtPositioning.coordinate(locationDelegate.latitude, locationDelegate.longitude)
zoomLevel: 15
plugin: Plugin {
name: "osm"
PluginParameter {
name: "osm.useragent"
value: Application.name + "/" + Application.version + " (kde-devel@kde.org)"
}
PluginParameter {
name: "osm.mapping.providersrepository.address"
value: "https://autoconfig.kde.org/qtlocation/"
}
}
MapCircle {
radius: 1500 / map.zoomLevel
color: Kirigami.Theme.highlightColor
border.color: Kirigami.Theme.linkColor
border.width: Kirigami.Units.devicePixelRatio * 2
smooth: true
opacity: 0.25
center: QtPositioning.coordinate(latitude, longitude)
}
onCopyrightLinkActivated: {
Qt.openUrlExternally(link)
}
TapHandler {
acceptedButtons: Qt.LeftButton
onTapped: {
let map = fullScreenMap.createObject(parent, {content: model.content});
map.open()
}
onLongPressed: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
}
TapHandler {
acceptedButtons: Qt.RightButton
onTapped: openMessageContext(author, model.message, eventId, toolTip, eventType, model.formattedBody ?? model.body, parent.selectedText)
}
}
Component {
id: fullScreenMap
FullScreenMap { }
}
}
}

View File

@@ -209,6 +209,18 @@ Kirigami.OverlayDrawer {
})
}
}
Kirigami.BasicListItem {
id: locationsButton
icon: "map-flat"
text: i18n("Show locations for this room")
onClicked: pageStack.pushDialogLayer("qrc:/LocationsPage.qml", {
room: room
}, {
title: i18nc("Locations on a map", "Locations")
})
}
Kirigami.BasicListItem {
id: favouriteButton

View File

@@ -107,5 +107,8 @@
<file alias="EmojiDelegate.qml">qml/Component/Emoji/EmojiDelegate.qml</file>
<file alias="EmojiGrid.qml">qml/Component/Emoji/EmojiGrid.qml</file>
<file alias="SearchPage.qml">qml/Page/SearchPage.qml</file>
<file alias="LocationDelegate.qml">qml/Component/Timeline/LocationDelegate.qml</file>
<file alias="FullScreenMap.qml">qml/Component/FullScreenMap.qml</file>
<file alias="LocationsPage.qml">qml/Component/LocationPage.qml</file>
</qresource>
</RCC>