diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 38aab75f9..28e23c4ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,6 +57,7 @@ add_library(neochat STATIC models/reactionmodel.cpp delegatesizehelper.cpp models/livelocationsmodel.cpp + models/locationsmodel.cpp ) ecm_qt_declare_logging_category(neochat diff --git a/src/main.cpp b/src/main.cpp index 23dbcbb72..0fb4a70d9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,6 +58,7 @@ #include "models/imagepacksmodel.h" #include "models/keywordnotificationrulemodel.h" #include "models/livelocationsmodel.h" +#include "models/locationsmodel.h" #include "models/messageeventmodel.h" #include "models/messagefiltermodel.h" #include "models/publicroomlistmodel.h" @@ -249,6 +250,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.kde.neochat", 1, 0, "StateFilterModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SearchModel"); qmlRegisterType("org.kde.neochat", 1, 0, "LiveLocationsModel"); + qmlRegisterType("org.kde.neochat", 1, 0, "LocationsModel"); #ifdef QUOTIENT_07 qmlRegisterType("org.kde.neochat", 1, 0, "PollHandler"); #endif diff --git a/src/models/locationsmodel.cpp b/src/models/locationsmodel.cpp new file mode 100644 index 000000000..717b03574 --- /dev/null +++ b/src/models/locationsmodel.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// 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(*event)) { + continue; + } + if (event->contentJson()["msgtype"] == "m.location") { + const auto &e = *event; + addLocation(eventCast(&e)); + } + } + connect(m_room, &NeoChatRoom::aboutToAddHistoricalMessages, this, [=](const auto &events) { + for (const auto &event : events) { + if (!is(*event)) { + continue; + } + if (event->contentJson()["msgtype"] == "m.location") { + const auto &e = *event; + addLocation(eventCast(&e)); + } + } + }); + connect(m_room, &NeoChatRoom::aboutToAddNewMessages, this, [=](const auto &events) { + for (const auto &event : events) { + if (!is(*event)) { + continue; + } + if (event->contentJson()["msgtype"] == "m.location") { + const auto &e = *event; + addLocation(eventCast(&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(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 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(); +} diff --git a/src/models/locationsmodel.h b/src/models/locationsmodel.h new file mode 100644 index 000000000..00280c64c --- /dev/null +++ b/src/models/locationsmodel.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "neochatroom.h" + +#include + +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 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 m_room; + + struct LocationData { + QString eventId; + float latitude; + float longitude; + QString text; + NeoChatUser *author; + }; + QList m_locations; + void addLocation(const Quotient::RoomMessageEvent *event); +}; diff --git a/src/qml/Component/FullScreenMap.qml b/src/qml/Component/FullScreenMap.qml new file mode 100644 index 000000000..eccfb705d --- /dev/null +++ b/src/qml/Component/FullScreenMap.qml @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// 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: OsmLocationPlugin.plugin + 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() + } +} diff --git a/src/qml/Component/LocationPage.qml b/src/qml/Component/LocationPage.qml new file mode 100644 index 000000000..65f45d064 --- /dev/null +++ b/src/qml/Component/LocationPage.qml @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// 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: OsmLocationPlugin.plugin + + 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" + } + } + } + } +} diff --git a/src/qml/Component/Timeline/LocationDelegate.qml b/src/qml/Component/Timeline/LocationDelegate.qml index e62da2d77..d559139cb 100644 --- a/src/qml/Component/Timeline/LocationDelegate.qml +++ b/src/qml/Component/Timeline/LocationDelegate.qml @@ -36,6 +36,7 @@ TimelineContainer { * a user's location. */ required property string asset + required property var content ColumnLayout { Layout.maximumWidth: root.contentMaxWidth @@ -92,6 +93,10 @@ TimelineContainer { TapHandler { acceptedButtons: Qt.LeftButton + onTapped: { + let map = fullScreenMap.createObject(parent, {content: root.content}); + map.open() + } onLongPressed: openMessageContext("") } TapHandler { @@ -99,5 +104,9 @@ TimelineContainer { onTapped: openMessageContext("") } } + Component { + id: fullScreenMap + FullScreenMap { } + } } } diff --git a/src/qml/Panel/RoomDrawer.qml b/src/qml/Panel/RoomDrawer.qml index c96aac6d7..0072c6120 100644 --- a/src/qml/Panel/RoomDrawer.qml +++ b/src/qml/Panel/RoomDrawer.qml @@ -143,6 +143,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 diff --git a/src/res.qrc b/src/res.qrc index 8da329c03..24c1e41d8 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -128,5 +128,7 @@ qml/Page/RoomList/SpaceDrawer.qml qml/Component/Timeline/OsmLocationPlugin.qml qml/Component/Timeline/LiveLocationDelegate.qml + qml/Component/FullScreenMap.qml + qml/Component/LocationPage.qml