1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-01 05:15:44 +02:00

feature: show and share user location

copyright OpenLayers v7.1.0: ol.css
copyright OpenLayers v7.1.0: ol.js

GitLab: #867

Change-Id: I4e01f6d9727d56541d1b44023f26959ebe4fbe26
Signed-off-by: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
This commit is contained in:
Nicolas Vengeon 2022-11-08 14:07:07 -05:00
parent 9bccc3805a
commit e5b54ad787
34 changed files with 1602 additions and 18 deletions

View file

@ -85,7 +85,8 @@ set(QT_MODULES
Core
Core5Compat
Multimedia
Widgets)
Widgets
Positioning)
if(NOT DEFINED WITH_WEBENGINE)
set(WITH_WEBENGINE true)
@ -96,7 +97,8 @@ if(WITH_WEBENGINE)
WebEngineCore
WebEngineQuick
WebChannel
WebEngineWidgets)
WebEngineWidgets
)
endif()
set(CMAKE_CXX_FLAGS
@ -176,6 +178,7 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/utils.cpp
${APP_SRC_DIR}/mainapplication.cpp
${APP_SRC_DIR}/messagesadapter.cpp
${APP_SRC_DIR}/positionmanager.cpp
${APP_SRC_DIR}/accountadapter.cpp
${APP_SRC_DIR}/calladapter.cpp
${APP_SRC_DIR}/conversationsadapter.cpp
@ -211,7 +214,9 @@ set(COMMON_SOURCES
${APP_SRC_DIR}/videodevices.cpp
${APP_SRC_DIR}/videoprovider.cpp
${APP_SRC_DIR}/callparticipantsmodel.cpp
${APP_SRC_DIR}/tipsmodel.cpp)
${APP_SRC_DIR}/tipsmodel.cpp
${APP_SRC_DIR}/positioning.cpp
)
set(COMMON_HEADERS
${APP_SRC_DIR}/avatarimageprovider.h
@ -228,6 +233,7 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/mainapplication.h
${APP_SRC_DIR}/qrimageprovider.h
${APP_SRC_DIR}/messagesadapter.h
${APP_SRC_DIR}/positionmanager.h
${APP_SRC_DIR}/accountadapter.h
${APP_SRC_DIR}/calladapter.h
${APP_SRC_DIR}/conversationsadapter.h
@ -267,7 +273,8 @@ set(COMMON_HEADERS
${APP_SRC_DIR}/videodevices.h
${APP_SRC_DIR}/videoprovider.h
${APP_SRC_DIR}/callparticipantsmodel.h
${APP_SRC_DIR}/tipsmodel.h)
${APP_SRC_DIR}/tipsmodel.h
${APP_SRC_DIR}/positioning.h)
if(WITH_WEBENGINE)
list(APPEND COMMON_SOURCES

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M12 41.5v-3h24.05v3Z"/></svg>

After

Width:  |  Height:  |  Size: 101 B

1
resources/icons/move.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="m24 44-8.15-8.15 2.2-2.2 4.45 4.45v-9.45h3v9.45l4.45-4.45 2.2 2.2ZM11.9 31.9 4 24l7.95-7.95 2.2 2.2L9.9 22.5h9.45v3H9.9l4.2 4.2Zm24.2 0-2.2-2.2 4.2-4.2h-9.4v-3h9.4l-4.2-4.2 2.2-2.2L44 24ZM22.5 19.3V9.9l-4.2 4.2-2.2-2.2L24 4l7.9 7.9-2.2 2.2-4.2-4.2v9.4Z"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M22.5 45.9v-3.75q-6.85-.7-11.4-5.25-4.55-4.55-5.25-11.4H2.1v-3h3.75q.7-6.85 5.25-11.4 4.55-4.55 11.4-5.25V2.1h3v3.75q6.85.7 11.4 5.25 4.55 4.55 5.25 11.4h3.75v3h-3.75q-.7 6.85-5.25 11.4-4.55 4.55-11.4 5.25v3.75Zm1.5-6.7q6.25 0 10.725-4.475T39.2 24q0-6.25-4.475-10.725T24 8.8q-6.25 0-10.725 4.475T8.8 24q0 6.25 4.475 10.725T24 39.2Zm0-7.7q-3.15 0-5.325-2.175Q16.5 27.15 16.5 24q0-3.15 2.175-5.325Q20.85 16.5 24 16.5q3.15 0 5.325 2.175Q31.5 20.85 31.5 24q0 3.15-2.175 5.325Q27.15 31.5 24 31.5Zm0-3q1.9 0 3.2-1.3 1.3-1.3 1.3-3.2 0-1.9-1.3-3.2-1.3-1.3-3.2-1.3-1.9 0-3.2 1.3-1.3 1.3-1.3 3.2 0 1.9 1.3 3.2 1.3 1.3 3.2 1.3Zm0-4.5Z"/></svg>

After

Width:  |  Height:  |  Size: 704 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M26.1 43.9v-3q2.25-.3 4.375-1.175T34.45 37.4l2.05 2.2q-2.3 1.9-4.9 2.95-2.6 1.05-5.5 1.35Zm13.45-7.4-2.15-2.05q1.35-1.8 2.25-3.875.9-2.075 1.25-4.475h3.05q-.4 3-1.525 5.625T39.55 36.5Zm1.35-14.6q-.35-2.4-1.25-4.45-.9-2.05-2.25-3.9l2.15-2.05q1.9 2.5 2.9 4.95t1.5 5.45Zm-19.05 22q-7.6-.85-12.7-6.525Q4.05 31.7 4.05 24t5.1-13.375q5.1-5.675 12.7-6.525v3q-6.35.85-10.575 5.675T7.05 24q0 6.4 4.225 11.225Q15.5 40.05 21.85 40.9ZM34.5 10.6q-1.95-1.35-4.075-2.225T26.2 7.1v-3q2.7.4 5.35 1.45Q34.2 6.6 36.5 8.4ZM24 34.55q-4.25-3.6-6.3-6.675-2.05-3.075-2.05-5.675 0-3.95 2.525-6.275T24 13.6q3.3 0 5.825 2.325Q32.35 18.25 32.35 22.2q0 2.6-2.05 5.675-2.05 3.075-6.3 6.675Zm0-10.45q.95 0 1.6-.65.65-.65.65-1.6 0-.85-.65-1.55-.65-.7-1.6-.7-.95 0-1.6.7-.65.7-.65 1.55 0 .95.65 1.6.65.65 1.6.65Z"/></svg>

After

Width:  |  Height:  |  Size: 859 B

View file

@ -48,6 +48,7 @@ AbstractButton {
property var preferredWidth
property real textLeftPadding
property real textRightPadding
property real fontSize: JamiTheme.wizardViewDescriptionFontPixelSize
Binding on width {
when: root.preferredWidth !== undefined ||
@ -183,7 +184,7 @@ AbstractButton {
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
color: contentColorProvider
font.pixelSize: JamiTheme.wizardViewDescriptionFontPixelSize
font.pixelSize: fontSize
}
}
}

View file

@ -299,6 +299,16 @@ Item {
property string raiseHand: qsTr("Raise hand")
property string layoutSettings: qsTr("Layout settings")
// Share location
property string shareLocation: qsTr("Share location")
property string stopSharingLocation: qsTr("Stop sharing location")
property string shortSharing: qsTr("10 minutes")
property string longSharing: qsTr("One hour")
property string minutesLeft: qsTr("%1 minutes left")
property string minuteLeft: qsTr("%1 minute left")
property string locationServicesError: qsTr("Jami needs to access to your location.\nIn Device Settings, please turn on Location Services.\nOther participants' location can still be received.")
property string locationServicesClosedError: qsTr("Please check your Internet connection.")
// Chatview header
property string hideChat: qsTr("Hide chat")
property string placeAudioCall: qsTr("Place audio call")

View file

@ -210,6 +210,10 @@ Item {
property color messageWebViewFooterButtonImageColor: darkTheme ? "#838383" : "#656565"
property color chatviewUsernameColor : "#A7A7A7"
//mapPosition
property color mapButtonsOverlayColor: darkTheme ? "#000000" : "#f0f0f0"
property color mapButtonColor: darkTheme ? "#f0f0f0" : "#000000"
// Files To Send Container
property color removeFileButtonColor: Qt.rgba(96, 95, 97, 0.5)
@ -314,6 +318,7 @@ Item {
property real preferredDialogWidth: 400
property real preferredDialogHeight: 300
property real minimumPreviewWidth: 120
property real minimumMapWidth: 230
property real pluginHandlersPopupViewHeight: 200
property real pluginHandlersPopupViewDelegateHeight: 50
property real secondaryDialogDimension: 500
@ -463,6 +468,9 @@ Item {
property real tipBoxTitleFontSize: calcSize(13)
property real tipBoxContentFontSize: calcSize(12)
//sharePosition
property real timerButtonsFontSize: calcSize(11)
//Popups
property real popuptextSize: calcSize(15)
property real popupButtonsMargin: 20

View file

@ -30,6 +30,7 @@
class ConversationListModel final : public ConversationListModelBase
{
Q_OBJECT
QML_PROPERTY(MapStringString, position)
public:
explicit ConversationListModel(LRCInstance* instance, QObject* parent = nullptr);
@ -49,7 +50,8 @@ public:
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
Q_INVOKABLE void setFilterRequests(bool filterRequests);
Q_INVOKABLE void ignoreFiltering(const QStringList& highlighted) {
Q_INVOKABLE void ignoreFiltering(const QStringList& highlighted)
{
ignored_ = highlighted;
}

View file

@ -410,7 +410,10 @@ Rectangle {
objectName: "chatView"
visible: false
Component.onCompleted: MessagesAdapter.setQmlObject(this)
Component.onCompleted: {
MessagesAdapter.setQmlObject(this)
PositionManager.setQmlObject(this)
}
}
NewSwarmPage {

View file

@ -47,10 +47,19 @@ Rectangle {
color: JamiTheme.chatviewBgColor
HostPopup {
property string currentConvId: CurrentConversation.id
onCurrentConvIdChanged: PositionManager.setMapActive(false);
Loader {
id: mapLoader
active: PositionManager.isMapActive
z: 10
source: WITH_WEBENGINE ? "qrc:/webengine/map/MapPosition.qml" : ""
}
HostPopup {
id: hostPopup
}
ColumnLayout {
anchors.fill: root

View file

@ -18,6 +18,7 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import net.jami.Models 1.1
import net.jami.Constants 1.1
@ -185,6 +186,11 @@ Rectangle {
emojiPickerLoader.openEmojiPicker()
}
onShowMapClicked: {
PositionManager.setMapActive(true);
}
onSendFileButtonClicked: jamiFileDialog.open()
onSendMessageButtonClicked: {
// Send text message

View file

@ -38,6 +38,7 @@ ColumnLayout {
signal sendFileButtonClicked
signal audioRecordMessageButtonClicked
signal videoRecordMessageButtonClicked
signal showMapClicked
signal emojiButtonClicked
implicitHeight: messageBarRowLayout.height
@ -129,6 +130,27 @@ ColumnLayout {
Component.onCompleted: JamiQmlUtils.videoRecordMessageButtonObj = videoRecordMessageButton
}
PushButton {
id: showMapButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
visible: WITH_WEBENGINE
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize
toolTipText: JamiStrings.shareLocation
source: JamiResources.share_location_svg
normalColor: JamiTheme.primaryBackgroundColor
imageColor: JamiTheme.messageWebViewFooterButtonImageColor
onClicked: root.showMapClicked()
}
MessageBarTextArea {
id: textArea

131
src/app/positioning.cpp Normal file
View file

@ -0,0 +1,131 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "positioning.h"
#include <QJsonObject>
#include <QJsonDocument>
Positioning::Positioning(QString uri, QObject* parent)
: QObject(parent)
, uri_(uri)
{
source_ = QGeoPositionInfoSource::createDefaultSource(this);
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &Positioning::requestPosition);
timer->start(2000);
connect(source_, &QGeoPositionInfoSource::errorOccurred, this, &Positioning::slotError);
connect(source_, &QGeoPositionInfoSource::positionUpdated, this, &Positioning::positionUpdated);
// if location services are activated, positioning will be activated automatically
connect(source_,
&QGeoPositionInfoSource::supportedPositioningMethodsChanged,
this,
&Positioning::locationServicesActivated);
}
Positioning::~Positioning()
{
sendStopSharingMsg();
}
void
Positioning::start()
{
if (source_ && !isPositioning) {
source_->startUpdates();
isPositioning = true;
}
}
void
Positioning::stop()
{
if (source_ && isPositioning)
source_->stopUpdates();
isPositioning = false;
}
void
Positioning::sendStopSharingMsg()
{
QJsonObject jsonObj;
jsonObj.insert("type", QJsonValue("Stop"));
QJsonDocument doc(jsonObj);
QString strJson(doc.toJson(QJsonDocument::Compact));
Q_EMIT newPosition(uri_, strJson, -1, "");
}
QString
Positioning::convertToJson(const QGeoPositionInfo& info)
{
QJsonObject jsonObj;
jsonObj.insert("type", QJsonValue("Position"));
jsonObj.insert("lat", QJsonValue(info.coordinate().latitude()));
jsonObj.insert("long", QJsonValue(info.coordinate().longitude()));
jsonObj.insert("time", QJsonValue(info.timestamp().toMSecsSinceEpoch()));
QJsonDocument doc(jsonObj);
QString strJson(doc.toJson(QJsonDocument::Compact));
return strJson;
}
void
Positioning::setUri(QString uri)
{
uri_ = uri;
}
void
Positioning::positionUpdated(const QGeoPositionInfo& info)
{
Q_EMIT positioningError("");
Q_EMIT newPosition(uri_, convertToJson(info), -1, "");
}
void
Positioning::requestPosition()
{
if (source_)
source_->requestUpdate();
}
void
Positioning::locationServicesActivated()
{
Q_EMIT positioningError("");
start();
}
static QString
errorToString(QGeoPositionInfoSource::Error error)
{
if (error == 0) {
return QObject::tr("locationServicesError");
}
if (error == 1) {
return QObject::tr("locationServicesClosedError");
}
return QObject::tr("locationServicesUnknownError");
}
void
Positioning::slotError(QGeoPositionInfoSource::Error error)
{
Q_EMIT positioningError(errorToString(error));
}

73
src/app/positioning.h Normal file
View file

@ -0,0 +1,73 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QtPositioning/QGeoPositionInfoSource>
#include <QObject>
#include <QString>
#include <QTimer>
class Positioning : public QObject
{
Q_OBJECT
public:
Positioning(QString uri, QObject* parent = 0);
~Positioning();
/**
* start to retreive the current position
*/
void start();
/**
* stop to retreive the current position
*/
void stop();
/**
* send a stop signal to other peers to tell them
* you stoped sharing yout position
*/
void sendStopSharingMsg();
QString convertToJson(const QGeoPositionInfo& info);
void setUri(QString uri);
private Q_SLOTS:
void slotError(QGeoPositionInfoSource::Error error);
void positionUpdated(const QGeoPositionInfo& info);
/**
* Force to send position at regular intervals
*/
void requestPosition();
/**
* Triggered when location services are activated
*/
void locationServicesActivated();
Q_SIGNALS:
void newPosition(const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId);
void positioningError(const QString error);
private:
QString uri_;
QGeoPositionInfoSource* source_ = nullptr;
bool isPositioning = false;
};

252
src/app/positionmanager.cpp Normal file
View file

@ -0,0 +1,252 @@
#include "positionmanager.h"
#include "qtutils.h"
#include <QApplication>
#include <QBuffer>
#include <QList>
#include <QTime>
#include <QJsonDocument>
#include <QImageReader>
PositionManager::PositionManager(LRCInstance* instance, QObject* parent)
: QmlAdapterBase(instance, parent)
{
timerTimeLeftSharing_ = new QTimer(this);
timerStopSharing_ = new QTimer(this);
connect(timerTimeLeftSharing_, &QTimer::timeout, [=] {
set_timeSharingRemaining(timerStopSharing_->remainingTime());
});
connect(timerStopSharing_, &QTimer::timeout, [=] { stopSharingPosition(); });
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
set_mapAutoOpening(true);
});
}
void
PositionManager::safeInit()
{
connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, [this]() {
connectConversationModel();
localPositioning_->setUri(lrcInstance_->getCurrentAccountInfo().profileInfo.uri);
});
localPositioning_.reset(new Positioning(lrcInstance_->getCurrentAccountInfo().profileInfo.uri));
connectConversationModel();
}
void
PositionManager::connectConversationModel()
{
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
QObject::connect(currentConversationModel,
&ConversationModel::newPosition,
this,
&PositionManager::onPositionReceived,
Qt::UniqueConnection);
}
void
PositionManager::startPositioning()
{
sharingUris_.clear();
localPositioning_->start();
connect(localPositioning_.get(),
&Positioning::newPosition,
this,
&PositionManager::onPositionReceived,
Qt::UniqueConnection);
connect(localPositioning_.get(),
&Positioning::positioningError,
this,
&PositionManager::onPositionErrorReceived,
Qt::UniqueConnection);
}
void
PositionManager::stopPositioning()
{
localPositioning_->stop();
}
void
PositionManager::onOwnPositionReceived(const QString& peerId, const QString& body)
{
try {
Q_FOREACH (const auto& id, positionShareConvIds_) {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(id);
Q_FOREACH (const QString& uri, convInfo.participantsUris()) {
if (peerId != uri) {
lrcInstance_->getCurrentAccountInfo()
.contactModel->sendDhtMessage(uri, body, APPLICATION_GEO);
}
}
}
} catch (const std::exception& e) {
qDebug() << Q_FUNC_INFO << e.what();
}
}
void
PositionManager::sharePosition(int maximumTime)
{
connect(localPositioning_.get(),
&Positioning::newPosition,
this,
&PositionManager::onOwnPositionReceived,
Qt::UniqueConnection);
try {
startPositionTimers(maximumTime);
const auto convUid = lrcInstance_->get_selectedConvUid();
positionShareConvIds_.append(convUid);
Q_EMIT positionShareConvIdsChanged();
} catch (...) {
qDebug() << "Exception during sharePosition:";
}
}
void
PositionManager::stopSharingPosition()
{
localPositioning_->sendStopSharingMsg();
stopPositionTimers();
set_positionShareConvIds({});
}
void
PositionManager::setMapActive(bool state)
{
set_isMapActive(state);
Q_EMIT isMapActiveChanged();
}
QString
PositionManager::getAvatar(const QString& uri)
{
QString avatarBase64;
QByteArray ba;
QBuffer bu(&ba);
auto& accInfo = lrcInstance_->getCurrentAccountInfo();
auto currentAccountUri = accInfo.profileInfo.uri;
if (currentAccountUri == uri) {
// use accountPhoto
Utils::accountPhoto(lrcInstance_, accInfo.id).save(&bu, "PNG");
} else {
// use contactPhoto
Utils::contactPhoto(lrcInstance_, uri).save(&bu, "PNG");
}
return ba.toBase64();
}
QVariantMap
PositionManager::parseJsonPosition(const QString& body, const QString& peerId)
{
QJsonDocument temp = QJsonDocument::fromJson(body.toUtf8());
QJsonObject jsonObject = temp.object();
QVariantMap pos;
for (auto i = jsonObject.begin(); i != jsonObject.end(); i++) {
if (i.key() == "long")
pos["long"] = i.value().toVariant();
if (i.key() == "lat")
pos["lat"] = i.value().toVariant();
if (i.key() == "type")
pos["type"] = i.value().toVariant();
if (i.key() == "time")
pos["time"] = i.value().toVariant();
pos["author"] = peerId;
}
return pos;
}
void
PositionManager::startPositionTimers(int timeSharing)
{
set_timeSharingRemaining(timeSharing);
timerTimeLeftSharing_->start(1000);
timerStopSharing_->start(timeSharing);
}
void
PositionManager::stopPositionTimers()
{
set_timeSharingRemaining(0);
timerTimeLeftSharing_->stop();
timerStopSharing_->stop();
}
void
PositionManager::onPositionErrorReceived(const QString error)
{
Q_EMIT positioningError(error);
}
void
PositionManager::onPositionReceived(const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId)
{
// only show shared positions from contacts in the current conversation
const auto& convParticipants = lrcInstance_
->getConversationFromConvUid(
lrcInstance_->get_selectedConvUid())
.participantsUris();
bool isPeerIdInConv = (std::find(convParticipants.begin(), convParticipants.end(), peerId)
!= convParticipants.end());
if (!isPeerIdInConv)
return;
QVariantMap newPosition = parseJsonPosition(body, peerId);
auto getShareInfo = [&](bool update) -> QVariantMap {
QVariantMap shareInfo;
shareInfo["author"] = peerId;
if (!update) {
shareInfo["avatar"] = getAvatar(peerId);
}
shareInfo["long"] = newPosition["long"];
shareInfo["lat"] = newPosition["lat"];
return shareInfo;
};
auto endSharing = newPosition["type"] == "Stop";
if (!endSharing) {
// open map on position reception
if (!isMapActive_ && mapAutoOpening_
&& peerId != lrcInstance_->getCurrentAccountInfo().profileInfo.uri) {
set_isMapActive(true);
}
}
auto iter = std::find(sharingUris_.begin(), sharingUris_.end(), peerId);
if (iter == sharingUris_.end()) {
// New share
if (!endSharing) {
sharingUris_.insert(peerId);
Q_EMIT positionShareAdded(getShareInfo(false));
}
} else {
// Update/remove existing
if (endSharing) {
// Remove (avoid self)
if (peerId != lrcInstance_->getCurrentAccountInfo().profileInfo.uri) {
sharingUris_.remove(peerId);
Q_EMIT positionShareRemoved(peerId);
// close the map if you're not sharing and the only remaining position is yours
if (!positionShareConvIds_.length() && sharingUris_.size() == 1
&& sharingUris_.contains(
lrcInstance_->getCurrentAccountInfo().profileInfo.uri)) {
set_isMapActive(false);
}
}
} else {
// Update
Q_EMIT positionShareUpdated(getShareInfo(true));
}
}
}

74
src/app/positionmanager.h Normal file
View file

@ -0,0 +1,74 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "lrcinstance.h"
#include "qmladapterbase.h"
#include "positioning.h"
#include <QObject>
#include <QString>
class PositionManager : public QmlAdapterBase
{
Q_OBJECT
QML_RO_PROPERTY(bool, isMapActive)
QML_RO_PROPERTY(int, timeSharingRemaining)
QML_PROPERTY(QList<QString>, positionShareConvIds)
QML_PROPERTY(bool, mapAutoOpening)
public:
explicit PositionManager(LRCInstance* instance, QObject* parent = nullptr);
~PositionManager() = default;
Q_SIGNALS:
void positioningError(const QString error);
void positionShareAdded(const QVariantMap& shareInfo);
void positionShareUpdated(const QVariantMap& posInfo);
void positionShareRemoved(const QString& uri);
protected:
void safeInit() override;
QString getAvatar(const QString& peerId);
QVariantMap parseJsonPosition(const QString& body, const QString& peerId);
void positionWatchDog();
void startPositionTimers(int timeSharing);
void stopPositionTimers();
Q_INVOKABLE void connectConversationModel();
Q_INVOKABLE void setMapActive(bool state);
Q_INVOKABLE void sharePosition(int maximumTime);
Q_INVOKABLE void startPositioning();
Q_INVOKABLE void stopPositioning();
Q_INVOKABLE void stopSharingPosition();
private Q_SLOTS:
void onPositionErrorReceived(const QString error);
void onPositionReceived(const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId);
void onOwnPositionReceived(const QString& peerId, const QString& body);
private:
std::unique_ptr<Positioning> localPositioning_;
QTimer* timerTimeLeftSharing_ = nullptr;
QTimer* timerStopSharing_ = nullptr;
QSet<QString> sharingUris_;
};

View file

@ -24,6 +24,7 @@
#include "contactadapter.h"
#include "pluginadapter.h"
#include "messagesadapter.h"
#include "positionmanager.h"
#include "tipsmodel.h"
#include "previewengine.h"
#include "utilsadapter.h"
@ -110,6 +111,7 @@ registerTypes(QQmlEngine* engine,
// setup the adapters (their lifetimes are that of MainApplication)
auto callAdapter = new CallAdapter(systemTray, lrcInstance, parent);
auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, parent);
auto positionManager = new PositionManager(lrcInstance, parent);
auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent);
auto avAdapter = new AvAdapter(lrcInstance, parent);
auto contactAdapter = new ContactAdapter(lrcInstance, parent);
@ -126,6 +128,7 @@ registerTypes(QQmlEngine* engine,
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, callAdapter, "CallAdapter");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, tipsModel, "TipsModel");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, messagesAdapter, "MessagesAdapter");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, positionManager, "PositionManager");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, conversationsAdapter, "ConversationsAdapter");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, avAdapter, "AvAdapter");
QML_REGISTERSINGLETONTYPE_POBJECT(NS_ADAPTERS, contactAdapter, "ContactAdapter");

View file

@ -817,6 +817,9 @@ QByteArray
Utils::QByteArrayFromFile(const QString& filename)
{
QFile file(filename);
if (!file.exists()) {
qDebug() << "QByteArrayFromFile: file does not exist" << filename;
}
if (file.open(QIODevice::ReadOnly)) {
return file.readAll();
} else {

View file

@ -0,0 +1,351 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import QtWebEngine
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
Rectangle {
id: mapPopup
x: xPos
y: yPos
width: isFullScreen ? root.width : windowSize
height: isMinimised
? buttonOverlay.height + buttonsChoseSharing.height + 30
: isFullScreen ? root.height - yPos : windowSize
property bool isFullScreen: false
property bool isMinimised: false
property real windowSize: windowPreferedSize > JamiTheme.minimumMapWidth
? windowPreferedSize
: JamiTheme.minimumMapWidth
property real windowPreferedSize: root.width > root.height
? root.height / 3
: root.width / 3
property real xPos: 0
property real yPos: JamiTheme.chatViewHeaderPreferredHeight
WebEngineView {
id: webView
width: parent.width
height: parent.height
property string mapHtml: ":/webengine/map/map.html"
property string olCss: ":/webengine/map/ol.css"
property string mapJs: "../../webengine/map/map.js"
property string olJs: "../../webengine/map/ol.js"
property bool isLoaded: false
property var positionList: PositionManager.positionList;
property var avatarPositionList: PositionManager.avatarPositionList;
property bool isSharing: (PositionManager.positionShareConvIds.length !== 0 )
function loadScripts () {
var scriptMapJs = {
sourceUrl: Qt.resolvedUrl(mapJs),
injectionPoint: WebEngineScript.DocumentReady,
worldId: WebEngineScript.MainWorld
}
var scriptOlJs = {
sourceUrl: Qt.resolvedUrl(olJs),
injectionPoint: WebEngineScript.DocumentReady,
worldId: WebEngineScript.MainWorld
}
userScripts.collection = [ scriptOlJs, scriptMapJs ]
}
Connections {
target: PositionManager
function onPositionShareAdded(shareInfo) {
if(webView.isLoaded) {
var curLong = shareInfo.long
var curLat = shareInfo.lat
webView.runJavaScript("newPosition([" + curLong + "," + curLat + "], '" + shareInfo.author + "', '" + shareInfo.avatar + "' )" );
webView.runJavaScript("zoomTolayersExtent()" );
}
}
function onPositionShareUpdated(shareInfo) {
if(webView.isLoaded) {
var curLong = shareInfo.long
var curLat = shareInfo.lat
webView.runJavaScript("updatePosition([" + curLong + "," + curLat + "], '" + shareInfo.author + "' )" );
}
}
function onPositionShareRemoved(author) {
if(webView.isLoaded) {
webView.runJavaScript("removePosition( '" + author + "' )" );
webView.runJavaScript("zoomTolayersExtent()" );
}
}
}
Component.onDestruction: {
PositionManager.stopSharingPosition();
PositionManager.stopPositioning();
}
Component.onCompleted: {
loadHtml(UtilsAdapter.qStringFromFile(mapHtml), mapHtml)
loadScripts()
}
onLoadingChanged: function (loadingInfo) {
if (loadingInfo.status === WebEngineView.LoadSucceededStatus) {
runJavaScript(UtilsAdapter.getStyleSheet("olcss",UtilsAdapter.qStringFromFile(olCss)))
webView.isLoaded = true
runJavaScript("setMapView([" + 0 + ","+ 0 + "], " + 1 + " );" );
PositionManager.startPositioning()
}
}
}
ColumnLayout {
id: buttonsChoseSharing
anchors.horizontalCenter: mapPopup.horizontalCenter
anchors.margins: 10
anchors.bottom: mapPopup.bottom
property bool shortSharing: true
RowLayout {
Layout.alignment: Qt.AlignHCenter
MaterialButton {
id: shortSharingButton
preferredWidth: text.contentWidth
visible: !webView.isSharing
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
text: JamiStrings.shortSharing
color: buttonsChoseSharing.shortSharing ? JamiTheme.buttonTintedBluePressed : JamiTheme.buttonTintedBlue
fontSize: JamiTheme.timerButtonsFontSize
onClicked: {
buttonsChoseSharing.shortSharing = true
}
}
MaterialButton {
id: longSharingButton
preferredWidth: text.contentWidth
visible: !webView.isSharing
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
text: JamiStrings.longSharing
color: !buttonsChoseSharing.shortSharing ? JamiTheme.buttonTintedBluePressed : JamiTheme.buttonTintedBlue
fontSize: JamiTheme.timerButtonsFontSize
onClicked: {
buttonsChoseSharing.shortSharing = false;
}
}
Rectangle {
radius: 10
width: textTimer.width + 15
height: textTimer.height + 15
color: JamiTheme.mapButtonsOverlayColor
visible: webView.isSharing && PositionManager.timeSharingRemaining
Text {
id: textTimer
anchors.centerIn: parent
color: JamiTheme.mapButtonColor
text: remainingTimeMs <= 1
? JamiStrings.minuteLeft.arg(remainingTimeMs)
: JamiStrings.minutesLeft.arg(remainingTimeMs)
Layout.alignment: Qt.AlignHCenter
property int remainingTimeMs: Math.ceil(PositionManager.timeSharingRemaining / 1000 / 60)
}
}
}
MaterialButton {
id: sharePositionButton
preferredWidth: text.contentWidth
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
text: webView.isSharing ? JamiStrings.stopSharingLocation : JamiStrings.shareLocation
color: isError
? JamiTheme.buttonTintedGreyInactive
: webView.isSharing ? JamiTheme.buttonTintedRed : JamiTheme.buttonTintedBlue
hoveredColor: isError
? JamiTheme.buttonTintedGreyInactive
: webView.isSharing ? JamiTheme.buttonTintedRedHovered : JamiTheme.buttonTintedBlueHovered
pressedColor: isError
? JamiTheme.buttonTintedGreyInactive
: webView.isSharing ? JamiTheme.buttonTintedRedPressed: JamiTheme.buttonTintedBluePressed
Layout.alignment: Qt.AlignHCenter
property bool isHovered: false
property string positioningError: "default"
property bool isError: positioningError.length
function errorString(posError) {
if (posError === "locationServicesError")
return JamiStrings.locationServicesError
return JamiStrings.locationServicesClosedError
}
onClicked: {
if (!isError) {
if (webView.isSharing) {
PositionManager.stopSharingPosition();
} else {
if (buttonsChoseSharing.shortSharing)
PositionManager.sharePosition(10 * 60 * 1000);
else
PositionManager.sharePosition(60 * 60 * 1000);
}
}
}
onHoveredChanged: {
isHovered = !isHovered
}
MaterialToolTip {
visible: sharePositionButton.isHovered
&& sharePositionButton.isError && (sharePositionButton.positioningError !== "default")
x: 0
y: 0
text: sharePositionButton.errorString(sharePositionButton.positioningError)
}
Connections {
target: PositionManager
function onPositioningError (err) {
sharePositionButton.positioningError = err;
}
}
}
}
Rectangle {
id: buttonOverlay
anchors.right: webView.right
anchors.top: webView.top
anchors.margins: 10
radius: 10
width: lay.width + 10
height: lay.height + 10
color: JamiTheme.mapButtonsOverlayColor
RowLayout {
id: lay
anchors.centerIn: parent
PushButton {
id: btnCenter
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.share_location_svg
onClicked: {
webView.runJavaScript("zoomTolayersExtent()" );
}
}
PushButton {
id: btnMove
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.move_svg
MouseArea {
anchors.fill: parent
drag.target: mapPopup
drag.minimumX: 0
drag.maximumX: root.width - mapPopup.width
drag.minimumY: 0
drag.maximumY: root.height - mapPopup.height
}
}
PushButton {
id: btnminimise
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: isMinimised
? JamiResources.close_fullscreen_24dp_svg
: JamiResources.minimize_svg
onClicked: {
isMinimised = !isMinimised
isFullScreen = false;
}
}
PushButton {
id: btnmaximise
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: isFullScreen? JamiResources.close_fullscreen_24dp_svg : JamiResources.open_in_full_24dp_svg
onClicked: {
if (!isFullScreen && !isMinimised) {
mapPopup.x = mapPopup.xPos
mapPopup.y = mapPopup.yPos
}
isFullScreen = !isFullScreen
isMinimised = false;
}
}
PushButton {
id: btnClose
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.round_close_24dp_svg
onClicked: {
PositionManager.setMapActive(false);
PositionManager.mapAutoOpening = false;
}
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
html,
body {
margin: 0;
height: 100%;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}

View file

@ -0,0 +1,37 @@
<!--
Copyright (C) 2022 Savoir-faire Linux Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
height: 100%
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
</body>
</html>

View file

@ -0,0 +1,134 @@
/*
* Copyright (C) 2022 Savoir-faire Linux Inc.
* Author: Nicolas Vengeon <nicolas.vengeon@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const {Map,View} = ol
const TileLayer = ol.layer.Tile
const ImageLayer = ol.layer.Image
const {OSM,ImageStatic} = ol.source
var basemap = new TileLayer({ source: new OSM() })
basemap.layer_type = "map"
var dict = []
const map = new Map({
target: 'map',
layers: [basemap],
view: new View({
center: ol.proj.fromLonLat([2.1734, 41.3851]),
zoom: 2
})
})
function setMapView(coordos, zoom) {
map.getView().setCenter(ol.proj.fromLonLat(coordos))
map.getView().setZoom(zoom)
}
function dynamicZoom(longMin, latMin, longMax, latMax) {
var coordMin = ol.proj.fromLonLat([longMin,latMin])
var coordMax = ol.proj.fromLonLat([longMax,latMax])
var extent = [coordMin[0],coordMin[1],coordMax[0],coordMax[1]]
map.getView().fit(extent, {size: map.getSize(), maxZoom: 16, duration:500,
padding: [80 ,80 ,80 ,80]})
}
var extent = [0,0,50,50]
var projection = new ol.proj.Projection({
code: 'local_image',
units: 'pixels',
extent: extent
})
var proj = new ol.proj.Projection({
code: 'static-image',
units: 'pixels',
extent: extent
})
function setSource (coordos, authorI,imageI) {
var coord = ol.proj.fromLonLat(coordos)
var pointFeature = new ol.Feature({
geometry: new ol.geom.Point(coord),
weight: 20
})
var preStyle = new ol.style.Icon({
src: "data:image/png;base64," + imageI})
//resize the image to 40 px
var image = preStyle.getImage()
if (!image.width) {
image.addEventListener('load', function () {
preStyle.setScale([40 / image.width, 40 / image.height])
})
} else {
preStyle.setScale([40 / image.width, 40 / image.height])
}
var iconStyle = new ol.style.Style({
image: preStyle
})
pointFeature.setStyle(iconStyle)
var vectorSource = new ol.source.Vector({
features: [pointFeature],
})
return vectorSource
}
function newPosition (coordos, authorI, image) {
vectorSource = setSource(coordos, authorI, image)
var iconLayer = new ol.layer.Vector({source: vectorSource})
iconLayer.layer_type = authorI
map.addLayer(iconLayer)
}
function updatePosition (coordos, authorI) {
var coord = ol.proj.fromLonLat(coordos);
var layerArray = map.getLayers().getArray();
for (var i = 0; i < layerArray.length; i++ ){
if(layerArray[i].layer_type === authorI) {
layerArray[i].getSource().getFeatures()[0].getGeometry().setCoordinates(coord)
return
}
}
}
function zoomTolayersExtent() {
var ext = ol.extent.createEmpty();
var layerArray = map.getLayers().getArray();
for (var i = 0; i < layerArray.length; i++ ){
if(layerArray[i].layer_type !== "map") {
ext = ol.extent.extend(ext, layerArray[i].getSource().getExtent());
}
}
map.getView().fit(ext, {size: map.getSize(), maxZoom: 16, duration:500,
padding: [80 ,80 ,80 ,80]})
}
function removePosition (authorI) {
var layerArray = map.getLayers().getArray();
for (var i = 0; i < layerArray.length; i++ ){
if(layerArray[i].layer_type === authorI) {
map.removeLayer(layerArray[i])
return
}
}
}

View file

@ -0,0 +1,371 @@
/*
* BSD 2-Clause License
*
* Copyright 2005-present, OpenLayers Contributors All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
:root,
:host {
--ol-background-color: white;
--ol-accent-background-color: #F5F5F5;
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
--ol-foreground-color: #333333;
--ol-subtle-foreground-color: #666666;
--ol-brand-color: #00AAFF;
}
.ol-box {
box-sizing: border-box;
border-radius: 2px;
border: 1.5px solid var(--ol-background-color);
background-color: var(--ol-partial-background-color);
}
.ol-mouse-position {
top: 8px;
right: 8px;
position: absolute;
}
.ol-scale-line {
background: var(--ol-partial-background-color);
border-radius: 4px;
bottom: 8px;
left: 8px;
padding: 2px;
position: absolute;
}
.ol-scale-line-inner {
border: 1px solid var(--ol-subtle-foreground-color);
border-top: none;
color: var(--ol-foreground-color);
font-size: 10px;
text-align: center;
margin: 1px;
will-change: contents, width;
transition: all 0.25s;
}
.ol-scale-bar {
position: absolute;
bottom: 8px;
left: 8px;
}
.ol-scale-bar-inner {
display: flex;
}
.ol-scale-step-marker {
width: 1px;
height: 15px;
background-color: var(--ol-foreground-color);
float: right;
z-index: 10;
}
.ol-scale-step-text {
position: absolute;
bottom: -5px;
font-size: 10px;
z-index: 11;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-text {
position: absolute;
font-size: 12px;
text-align: center;
bottom: 25px;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-singlebar {
position: relative;
height: 10px;
z-index: 9;
box-sizing: border-box;
border: 1px solid var(--ol-foreground-color);
}
.ol-scale-singlebar-even {
background-color: var(--ol-subtle-foreground-color);
}
.ol-scale-singlebar-odd {
background-color: var(--ol-background-color);
}
.ol-unsupported {
display: none;
}
.ol-viewport,
.ol-unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.ol-viewport canvas {
all: unset;
}
.ol-selectable {
-webkit-touch-callout: default;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
.ol-grabbing {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.ol-grab {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.ol-control {
position: absolute;
background-color: var(--ol-subtle-background-color);
border-radius: 4px;
}
.ol-zoom {
top: .5em;
left: .5em;
}
.ol-rotate {
top: .5em;
right: .5em;
transition: opacity .25s linear, visibility 0s linear;
}
.ol-rotate.ol-hidden {
opacity: 0;
visibility: hidden;
transition: opacity .25s linear, visibility 0s linear .25s;
}
.ol-zoom-extent {
top: 4.643em;
left: .5em;
}
.ol-full-screen {
right: .5em;
top: .5em;
}
.ol-control button {
display: block;
margin: 1px;
padding: 0;
color: var(--ol-subtle-foreground-color);
font-weight: bold;
text-decoration: none;
font-size: inherit;
text-align: center;
height: 1.375em;
width: 1.375em;
line-height: .4em;
background-color: var(--ol-background-color);
border: none;
border-radius: 2px;
}
.ol-control button::-moz-focus-inner {
border: none;
padding: 0;
}
.ol-zoom-extent button {
line-height: 1.4em;
}
.ol-compass {
display: block;
font-weight: normal;
will-change: transform;
}
.ol-touch .ol-control button {
font-size: 1.5em;
}
.ol-touch .ol-zoom-extent {
top: 5.5em;
}
.ol-control button:hover,
.ol-control button:focus {
text-decoration: none;
outline: 1px solid var(--ol-subtle-foreground-color);
color: var(--ol-foreground-color);
}
.ol-zoom .ol-zoom-in {
border-radius: 2px 2px 0 0;
}
.ol-zoom .ol-zoom-out {
border-radius: 0 0 2px 2px;
}
.ol-attribution {
text-align: right;
bottom: .5em;
right: .5em;
max-width: calc(100% - 1.3em);
display: flex;
flex-flow: row-reverse;
align-items: center;
}
.ol-attribution a {
color: var(--ol-subtle-foreground-color);
text-decoration: none;
}
.ol-attribution ul {
margin: 0;
padding: 1px .5em;
color: var(--ol-foreground-color);
text-shadow: 0 0 2px var(--ol-background-color);
font-size: 12px;
}
.ol-attribution li {
display: inline;
list-style: none;
}
.ol-attribution li:not(:last-child):after {
content: " ";
}
.ol-attribution img {
max-height: 2em;
max-width: inherit;
vertical-align: middle;
}
.ol-attribution button {
flex-shrink: 0;
}
.ol-attribution.ol-collapsed ul {
display: none;
}
.ol-attribution:not(.ol-collapsed) {
background: var(--ol-partial-background-color);
}
.ol-attribution.ol-uncollapsible {
bottom: 0;
right: 0;
border-radius: 4px 0 0;
}
.ol-attribution.ol-uncollapsible img {
margin-top: -.2em;
max-height: 1.6em;
}
.ol-attribution.ol-uncollapsible button {
display: none;
}
.ol-zoomslider {
top: 4.5em;
left: .5em;
height: 200px;
}
.ol-zoomslider button {
position: relative;
height: 10px;
}
.ol-touch .ol-zoomslider {
top: 5.5em;
}
.ol-overviewmap {
left: 0.5em;
bottom: 0.5em;
}
.ol-overviewmap.ol-uncollapsible {
bottom: 0;
left: 0;
border-radius: 0 4px 0 0;
}
.ol-overviewmap .ol-overviewmap-map,
.ol-overviewmap button {
display: block;
}
.ol-overviewmap .ol-overviewmap-map {
border: 1px solid var(--ol-subtle-foreground-color);
height: 150px;
width: 150px;
}
.ol-overviewmap:not(.ol-collapsed) button {
bottom: 0;
left: 0;
position: absolute;
}
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
.ol-overviewmap.ol-uncollapsible button {
display: none;
}
.ol-overviewmap:not(.ol-collapsed) {
background: var(--ol-subtle-background-color);
}
.ol-overviewmap-box {
border: 1.5px dotted var(--ol-subtle-foreground-color);
}
.ol-overviewmap .ol-overviewmap-box:hover {
cursor: move;
}

File diff suppressed because one or more lines are too long

View file

@ -185,6 +185,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
# Here we let find_package(<PackageName>...) try to find Qt 6.
# If it is found, find_package will succeed, and the CMake variable
# QT_VERSION_MAJOR will be set to 6.
if(QT6_VER AND QT6_PATH)
find_package(QT NAMES Qt6 REQUIRED
PATHS ${QT6_PATH} NO_DEFAULT_PATH)

View file

@ -108,7 +108,9 @@ public:
* @param body
* @return id from daemon
*/
uint64_t sendDhtMessage(const QString& uri, const QString& body) const;
uint64_t sendDhtMessage(const QString& uri,
const QString& body,
const QString& mimeType = {}) const;
/**
* Get best id for contact
* @param contactUri

View file

@ -439,6 +439,15 @@ public:
member::Role memberRole(const QString& conversationId, const QString& memberUri) const;
Q_SIGNALS:
/**
* Emitted when a conversation receives a new position
*/
void newPosition(const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId) const;
/**
* Emitted when a conversation receives a new interaction
* @param uid of conversation

View file

@ -65,7 +65,7 @@ to_type(const QString& type)
{
if (type == "INITIAL" || type == "initial")
return interaction::Type::INITIAL;
else if (type == "TEXT" || type == "text/plain")
else if (type == "TEXT" || type == TEXT_PLAIN)
return interaction::Type::TEXT;
else if (type == "CALL" || type == "application/call-history+json")
return interaction::Type::CALL;

View file

@ -569,8 +569,7 @@ CallbacksHandler::slotIncomingMessage(const QString& accountId,
match.captured(2).toInt(),
match.captured(3).toInt(),
e.second);
} else if (e.first.contains(
"text/plain")) { // we consider it as an usual message interaction
} else if (e.first.contains(TEXT_PLAIN)) { // we consider it as an usual message interaction
Q_EMIT incomingCallMessage(accountId, callId, from2, e.second);
}
}

View file

@ -1279,7 +1279,7 @@ void
CallModel::sendSipMessage(const QString& callId, const QString& body) const
{
MapStringString payloads;
payloads["text/plain"] = body;
payloads[TEXT_PLAIN] = body;
CallManager::instance().sendTextMessage(owner.id, callId, payloads, true /* not used */);
}

View file

@ -517,11 +517,16 @@ ContactModelPimpl::searchSipContact(const URI& query)
}
uint64_t
ContactModel::sendDhtMessage(const QString& contactUri, const QString& body) const
ContactModel::sendDhtMessage(const QString& contactUri,
const QString& body,
const QString& mimeType) const
{
// Send interaction
QMap<QString, QString> payloads;
payloads["text/plain"] = body;
if (mimeType.isEmpty())
payloads[TEXT_PLAIN] = body;
else
payloads[mimeType] = body;
auto msgId = ConfigurationManager::instance().sendTextMessage(QString(owner.id),
QString(contactUri),
payloads);

View file

@ -2213,7 +2213,7 @@ ConversationModelPimpl::initConversations()
timestamp = static_cast<uint64_t>(message.received);
} catch (...) {
}
addIncomingMessage(message.from, message.payloads["text/plain"], timestamp);
addIncomingMessage(message.from, message.payloads[TEXT_PLAIN], timestamp);
}
}
@ -3563,8 +3563,12 @@ ConversationModelPimpl::slotNewAccountMessage(const QString& accountId,
return;
for (const auto& payload : payloads.keys()) {
if (payload.contains("text/plain")) {
if (payload.contains(TEXT_PLAIN)) {
addIncomingMessage(peerId, payloads.value(payload), 0, msgId);
} else if (payload.contains(APPLICATION_GEO)) {
Q_EMIT linked.newPosition(peerId, payloads.value(payload), 0, msgId);
} else {
qWarning() << payload;
}
}
}

View file

@ -29,6 +29,7 @@
// Typedefs (required to avoid '<' and '>' in the DBus XML)
typedef QMap<QString, QString> MapStringString;
typedef QMap<QString, int> MapStringInt;
typedef QMap<QString, double> MapStringDouble;
typedef QVector<int> VectorInt;
typedef QVector<uint> VectorUInt;
typedef QVector<qulonglong> VectorULongLong;
@ -42,6 +43,8 @@ typedef QVector<QByteArray> VectorVectorByte;
typedef uint64_t DataTransferId;
constexpr static const char* TRUE_STR = "true";
constexpr static const char* TEXT_PLAIN = "text/plain";
constexpr static const char* APPLICATION_GEO = "application/geo";
constexpr static const char* FALSE_STR = "false";
// Adapted from libring libjami::DataTransferInfo