diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c269d51de..dfe5b2fe9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,6 +47,7 @@ add_library(neochat STATIC filetransferpseudojob.cpp models/searchmodel.cpp texthandler.cpp + models/locationsmodel.cpp ) add_executable(neochat-app diff --git a/src/main.cpp b/src/main.cpp index e6d53ed46..9a72f63fb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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("org.kde.neochat", 1, 0, "CompletionModel"); qmlRegisterType("org.kde.neochat", 1, 0, "StateModel"); qmlRegisterType("org.kde.neochat", 1, 0, "SearchModel"); + 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..e89d4cc3d --- /dev/null +++ b/src/models/locationsmodel.h @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#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); +}; \ No newline at end of file diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index c3c2041c5..616466464 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -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(&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 diff --git a/src/models/messageeventmodel.h b/src/models/messageeventmodel.h index 4add993a5..7b979643f 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messageeventmodel.h @@ -26,6 +26,7 @@ public: Encrypted, ReadMarker, Poll, + Location, Other, }; Q_ENUM(DelegateType); diff --git a/src/qml/Component/FullScreenMap.qml b/src/qml/Component/FullScreenMap.qml new file mode 100644 index 000000000..49ad91305 --- /dev/null +++ b/src/qml/Component/FullScreenMap.qml @@ -0,0 +1,70 @@ +// 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: Plugin { + name: "osm" + } + 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..5f94d246d --- /dev/null +++ b/src/qml/Component/LocationPage.qml @@ -0,0 +1,51 @@ +// 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: Plugin { + name: "osm" + } + + 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/EventDelegate.qml b/src/qml/Component/Timeline/EventDelegate.qml index 564c035de..e2f1605da 100644 --- a/src/qml/Component/Timeline/EventDelegate.qml +++ b/src/qml/Component/Timeline/EventDelegate.qml @@ -75,6 +75,11 @@ DelegateChooser { delegate: PollDelegate {} } + DelegateChoice { + roleValue: MessageEventModel.Location + delegate: LocationDelegate {} + } + DelegateChoice { roleValue: MessageEventModel.Other delegate: Item {} diff --git a/src/qml/Component/Timeline/LocationDelegate.qml b/src/qml/Component/Timeline/LocationDelegate.qml new file mode 100644 index 000000000..e442c1cd4 --- /dev/null +++ b/src/qml/Component/Timeline/LocationDelegate.qml @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella +// 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" + } + 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 { } + } + } +} diff --git a/src/qml/Panel/RoomDrawer.qml b/src/qml/Panel/RoomDrawer.qml index a0c44f95b..16b66ecd9 100644 --- a/src/qml/Panel/RoomDrawer.qml +++ b/src/qml/Panel/RoomDrawer.qml @@ -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 diff --git a/src/res.qrc b/src/res.qrc index 622257589..ef151e2f3 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -107,5 +107,8 @@ qml/Component/Emoji/EmojiDelegate.qml qml/Component/Emoji/EmojiGrid.qml qml/Page/SearchPage.qml + qml/Component/Timeline/LocationDelegate.qml + qml/Component/FullScreenMap.qml + qml/Component/LocationPage.qml