1
0
Fork 0
mirror of https://git.jami.net/savoirfairelinux/jami-client-qt.git synced 2025-08-04 14:55:43 +02:00

Feature: unpin location sharing map

- Refactoring
- Unpined map
- handle multiple maps (one map per account)

Change-Id: I2b0abf284ccfe27b986f03915c5942d721403211
Gitlab: #901
This commit is contained in:
Nicolas Vengeon 2022-12-05 15:44:04 -05:00 committed by Sébastien Blin
parent c36ccb5fa9
commit d06902e3b7
22 changed files with 1162 additions and 600 deletions

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M9 42q-1.2 0-2.1-.9Q6 40.2 6 39V9q0-1.2.9-2.1Q7.8 6 9 6h13.95v3H9v30h30V25.05h3V39q0 1.2-.9 2.1-.9.9-2.1.9Zm10.1-10.95L17 28.9 36.9 9H25.95V6H42v16.05h-3v-10.9Z"/></svg>

After

Width:  |  Height:  |  Size: 241 B

View file

@ -54,7 +54,9 @@ extern const QString defaultDownloadPath;
X(WindowGeometry, QRectF(qQNaN(), qQNaN(), 0., 0.)) \
X(WindowState, QWindow::AutomaticVisibility) \
X(EnableExperimentalSwarm, false) \
X(LANG, "SYSTEM")
X(LANG, "SYSTEM") \
X(PositionShareDuration, 15) \
X(PositionShareLimit, true)
/*
* A class to expose settings keys in both c++ and QML.

View file

@ -309,8 +309,12 @@ Item {
property string locationServicesError: qsTr("Your precise location could not be determined.\nIn Device Settings, please turn on \"Location Services\".\nOther participants' location can still be received.")
property string locationServicesClosedError: qsTr("Your precise location could not be determined. Please check your Internet connection.")
property string stopAllSharings: qsTr("Turn off location sharing");
property string stopConvSharing: qsTr("Stop location sharing in this conversation");
property string shortStopAllSharings: qsTr("Turn off sharing");
property string stopConvSharing: qsTr("Stop location sharing in this conversation (%1)");
property string stopSharingPopupBody: qsTr("Location is shared in several conversations");
property string unpinStopSharingTooltip: qsTr("Pin map to be able to share location or to turn off location in specific conversations");
property string stopSharingSeveralConversationTooltip: qsTr("Location is shared in several conversations, click to choose how to turn off location sharing")
property string shareLocationToolTip: qsTr("Share location to participants of this conversation (%1)");
property string minimizeMapTooltip: qsTr("Minimize");
property string maximizeMapTooltip: qsTr("Maximize");
property string reduceMapTooltip: qsTr("Reduce");
@ -318,6 +322,11 @@ Item {
property string dragMapTooltip: qsTr("Drag");
property string centerMapTooltip: qsTr("Center");
property string closeMapTooltip: qsTr("Close");
property string unpin: qsTr("Unpin");
property string pinWindow: qsTr("Pin");
property string positionShareDuration: qsTr("Position share duration");
property string positionShareLimit: qsTr("Limit the duration of location sharing");
property string locationSharingLabel: qsTr("Location sharing");
// Chatview header
property string hideChat: qsTr("Hide chat")
@ -694,6 +703,7 @@ Item {
// SmartList
property string clearText: qsTr("Clear Text")
property string conversations: qsTr("Conversations")
property string conversation: qsTr("Conversation")
property string searchResults: qsTr("Search Results")
// SmartList context menu

View file

@ -34,7 +34,7 @@ Rectangle {
id: root
property bool allMessagesLoaded
property var mapPositions: PositionManager.mapStatus
signal needToHideConversationInCall
signal messagesCleared
signal messagesLoaded
@ -51,18 +51,28 @@ Rectangle {
addMemberPanel.visible = false
}
function instanceMapObject() {
if (WITH_WEBENGINE) {
var component = Qt.createComponent("qrc:/webengine/map/MapPosition.qml");
var sprite = component.createObject(root, {maxWidth: root.width, maxHeight: root.height});
if (sprite === null) {
// Error Handling
console.log("Error creating object");
}
}
}
Connections {
target: PositionManager
function onOpenNewMap() {
instanceMapObject()
}
}
color: JamiTheme.chatviewBgColor
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

View file

@ -190,7 +190,7 @@ Rectangle {
}
onShowMapClicked: {
PositionManager.setMapActive(true);
PositionManager.setMapActive(CurrentAccount.id)
}
onSendFileButtonClicked: jamiFileDialog.open()

View file

@ -65,12 +65,33 @@ ColumnLayout {
spacing: JamiTheme.chatViewFooterRowSpacing
PushButton {
id: sendFileButton
id: showMapButton
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: marginSize
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
visible: WITH_WEBENGINE && !CurrentConversation.isSip
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize
toolTipText: JamiStrings.shareLocation
source: JamiResources.share_location_svg
normalColor: JamiTheme.primaryBackgroundColor
imageColor: JamiTheme.messageWebViewFooterButtonImageColor
onClicked: root.showMapClicked()
}
PushButton {
id: sendFileButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
radius: JamiTheme.chatViewFooterButtonRadius
preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
@ -130,27 +151,6 @@ ColumnLayout {
Component.onCompleted: JamiQmlUtils.videoRecordMessageButtonObj = videoRecordMessageButton
}
PushButton {
id: showMapButton
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
visible: WITH_WEBENGINE && !CurrentConversation.isSip
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

View file

@ -68,8 +68,8 @@ ItemDelegate {
imageId: UID
showPresenceIndicator: Presence !== undefined ? Presence : false
showSharePositionIndicator: PositionManager.isPositionSharedToConv(UID)
showSharedPositionIndicator: PositionManager.isConvSharingPosition(UID)
showSharePositionIndicator: PositionManager.isPositionSharedToConv(accountId, UID)
showSharedPositionIndicator: PositionManager.isConvSharingPosition(accountId, UID)
Layout.preferredWidth: JamiTheme.smartListAvatarSize
Layout.preferredHeight: JamiTheme.smartListAvatarSize
@ -77,10 +77,10 @@ ItemDelegate {
Connections {
target: PositionManager
function onPositionShareConvIdsCountChanged () {
avatar.showSharePositionIndicator = PositionManager.isPositionSharedToConv(UID)
avatar.showSharePositionIndicator = PositionManager.isPositionSharedToConv(accountId, UID)
}
function onSharingUrisCountChanged () {
avatar.showSharedPositionIndicator = PositionManager.isConvSharingPosition(UID)
avatar.showSharedPositionIndicator = PositionManager.isConvSharingPosition(accountId, UID)
}
}

View file

@ -21,14 +21,12 @@
#include <QJsonObject>
#include <QJsonDocument>
Positioning::Positioning(QString uri, QObject* parent)
Positioning::Positioning(QObject* parent)
: QObject(parent)
, uri_(uri)
{
source_ = QGeoPositionInfoSource::createDefaultSource(this);
QTimer* timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &Positioning::requestPosition);
timer->start(5000);
timer_ = new QTimer(this);
connect(timer_, &QTimer::timeout, this, &Positioning::requestPosition);
connect(source_, &QGeoPositionInfoSource::errorOccurred, this, &Positioning::slotError);
connect(source_, &QGeoPositionInfoSource::positionUpdated, this, &Positioning::positionUpdated);
// if location services are activated, positioning will be activated automatically
@ -41,6 +39,8 @@ Positioning::Positioning(QString uri, QObject* parent)
void
Positioning::start()
{
requestPosition();
timer_->start(10000);
if (source_ && !isPositioning) {
source_->startUpdates();
isPositioning = true;
@ -53,6 +53,7 @@ Positioning::stop()
if (source_ && isPositioning)
source_->stopUpdates();
isPositioning = false;
timer_->stop();
}
QString
@ -70,17 +71,11 @@ Positioning::convertToJson(const QGeoPositionInfo& info)
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, "");
Q_EMIT newPosition(convertToJson(info));
}
void

View file

@ -27,7 +27,7 @@ class Positioning : public QObject
Q_OBJECT
public:
Positioning(QString uri, QObject* parent = 0);
Positioning(QObject* parent = 0);
/**
* start to retreive the current position
*/
@ -42,8 +42,6 @@ public:
*/
QString convertToJson(const QGeoPositionInfo& info);
void setUri(QString uri);
private Q_SLOTS:
void slotError(QGeoPositionInfoSource::Error error);
void positionUpdated(const QGeoPositionInfo& info);
@ -57,15 +55,12 @@ private Q_SLOTS:
void locationServicesActivated();
Q_SIGNALS:
void newPosition(const QString& unused_AccountId,
const QString& peerId,
const QString& body,
const uint64_t& timestamp,
const QString& daemonId);
void newPosition(const QString& body);
void positioningError(const QString error);
private:
QString uri_;
QGeoPositionInfoSource* source_ = nullptr;
bool isPositioning = false;
QTimer* timer_;
};

View file

@ -1,7 +1,8 @@
#include "positionmanager.h"
#include "qtutils.h"
#include "appsettingsmanager.h"
#include "qtutils.h"
#include <QApplication>
#include <QBuffer>
#include <QList>
@ -9,31 +10,40 @@
#include <QJsonDocument>
#include <QImageReader>
PositionManager::PositionManager(SystemTray* systemTray, LRCInstance* instance, QObject* parent)
PositionManager::PositionManager(AppSettingsManager* settingsManager,
SystemTray* systemTray,
LRCInstance* instance,
QObject* parent)
: QmlAdapterBase(instance, parent)
, systemTray_(systemTray)
, settingsManager_(settingsManager)
{
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);
});
connect(lrcInstance_, &LRCInstance::currentAccountIdChanged, [this]() {
if (!localPositioning_) // Not yet initialized
return;
localPositioning_->setUri(lrcInstance_->getCurrentAccountInfo().profileInfo.uri);
});
set_isMapActive(false);
countdownTimer_ = new QTimer(this);
connect(countdownTimer_, &QTimer::timeout, this, &PositionManager::countdownUpdate);
connect(lrcInstance_,
&LRCInstance::selectedConvUidChanged,
this,
&PositionManager::onNewConversation,
Qt::UniqueConnection);
connect(lrcInstance_,
&LRCInstance::currentAccountIdChanged,
this,
&PositionManager::onNewAccount,
Qt::UniqueConnection);
connect(
this,
&PositionManager::localPositionReceived,
this,
[this](const QString& accountId, const QString& peerId, const QString& body) {
onPositionReceived(accountId, peerId, body, -1, "");
},
Qt::QueuedConnection);
}
void
PositionManager::safeInit()
{
localPositioning_.reset(new Positioning(lrcInstance_->getCurrentAccountInfo().profileInfo.uri));
localPositioning_.reset(new Positioning());
connectAccountModel();
}
@ -50,41 +60,36 @@ PositionManager::connectAccountModel()
void
PositionManager::startPositioning()
{
currentConvSharingUris_.clear();
localPositioning_->start();
connect(localPositioning_.get(),
&Positioning::newPosition,
this,
&PositionManager::onPositionReceived,
Qt::UniqueConnection);
if (localPositioning_)
localPositioning_->start();
connect(localPositioning_.get(),
&Positioning::positioningError,
this,
&PositionManager::onPositionErrorReceived,
Qt::UniqueConnection);
connect(
localPositioning_.get(),
&Positioning::newPosition,
this,
[this](const QString& body) { sendPosition(body, true); },
Qt::UniqueConnection);
}
void
PositionManager::stopPositioning()
{
localPositioning_->stop();
}
QString
PositionManager::getSelectedConvId()
{
return lrcInstance_->get_selectedConvUid();
if (localPositioning_)
localPositioning_->stop();
}
bool
PositionManager::isConvSharingPosition(const QString& convUri)
PositionManager::isConvSharingPosition(const QString& accountId, const QString& convUri)
{
const auto& convParticipants = lrcInstance_->getConversationFromConvUid(convUri)
.participantsUris();
Q_FOREACH (const auto& id, convParticipants) {
if (id != lrcInstance_->getCurrentAccountInfo().profileInfo.uri) {
if (objectListSharingUris_.contains(
QPair<QString, QString> {lrcInstance_->get_currentAccountId(), id})) {
if (objectListSharingUris_.contains(PositionKey {accountId, id})) {
return true;
}
}
@ -93,36 +98,53 @@ PositionManager::isConvSharingPosition(const QString& convUri)
}
void
PositionManager::loadPreviousLocations()
PositionManager::loadPreviousLocations(QString& accountId)
{
QVariantMap shareInfo;
for (auto it = objectListSharingUris_.begin(); it != objectListSharingUris_.end(); it++) {
QJsonObject jsonObj;
jsonObj.insert("type", QJsonValue("Position"));
jsonObj.insert("lat", it.value()->getLatitude().toString());
jsonObj.insert("long", it.value()->getLongitude().toString());
QJsonDocument doc(jsonObj);
QString strJson(doc.toJson(QJsonDocument::Compact));
onPositionReceived(it.key().first, it.key().second, strJson, -1, "");
if (it.key().first == accountId) {
QJsonObject jsonObj;
jsonObj.insert("type", QJsonValue("Position"));
jsonObj.insert("lat", it.value()->getLatitude().toString());
jsonObj.insert("long", it.value()->getLongitude().toString());
QJsonDocument doc(jsonObj);
QString strJson(doc.toJson(QJsonDocument::Compact));
// parse the position from json
QVariantMap positionReceived = parseJsonPosition(it.key().first,
it.key().second,
strJson);
addPositionToMap(it.key(), positionReceived);
}
}
}
QString
PositionManager::getmapTitle(QString& accountId, QString convId)
{
if (!convId.isEmpty() && !accountId.isEmpty()) {
return lrcInstance_->getAccountInfo(accountId).conversationModel->title(convId);
}
if (!accountId.isEmpty())
return lrcInstance_->getAccountInfo(accountId).registeredName;
return {};
}
bool
PositionManager::isPositionSharedToConv(const QString& convUid)
PositionManager::isPositionSharedToConv(const QString& accountId, const QString& convUid)
{
if (positionShareConvIds_.length()) {
auto iter = std::find(positionShareConvIds_.begin(),
positionShareConvIds_.end(),
QPair<QString, QString> {lrcInstance_->get_currentAccountId(),
convUid});
PositionKey {accountId, convUid});
return (iter != positionShareConvIds_.end());
}
return false;
}
void
PositionManager::sendPosition(const QString& body)
PositionManager::sendPosition(const QString& body, bool triggersLocalPosition)
{
// send position to positionShareConvIds_ participants
try {
Q_FOREACH (const auto& key, positionShareConvIds_) {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(key.second, key.first);
@ -137,6 +159,15 @@ PositionManager::sendPosition(const QString& body)
} catch (const std::exception& e) {
qDebug() << Q_FUNC_INFO << e.what();
}
if (triggersLocalPosition) {
// send own position to every account with an opened map
QMutexLocker lk(&mapStatusMutex_);
for (auto it = mapStatus_.begin(); it != mapStatus_.end(); it++) {
Q_EMIT localPositionReceived(it.key(),
lrcInstance_->getAccountInfo(it.key()).profileInfo.uri,
body);
}
}
}
void
@ -149,26 +180,17 @@ PositionManager::onWatchdogTimeout()
if (it != objectListSharingUris_.cend()) {
QString stopMsg("{\"type\":\"Stop\"}");
onPositionReceived(it.key().first, it.key().second, stopMsg, -1, "");
makeVisibleSharingButton(it.key().first);
}
}
void
PositionManager::sharePosition(int maximumTime)
PositionManager::sharePosition(int maximumTime, QString accountId, QString convId)
{
connect(
localPositioning_.get(),
&Positioning::newPosition,
this,
[&](const QString&, const QString&, const QString& body, const uint64_t&, const QString&) {
sendPosition(body);
},
Qt::UniqueConnection);
try {
startPositionTimers(maximumTime);
const auto convUid = lrcInstance_->get_selectedConvUid();
positionShareConvIds_.append(
QPair<QString, QString> {lrcInstance_->get_currentAccountId(), convUid});
if (settingsManager_->getValue(Settings::Key::PositionShareLimit) == true)
startPositionTimers(maximumTime);
positionShareConvIds_.append(PositionKey {accountId, convId});
set_positionShareConvIdsCount(positionShareConvIds_.size());
} catch (...) {
qDebug() << "Exception during sharePosition:";
@ -176,17 +198,46 @@ PositionManager::sharePosition(int maximumTime)
}
void
PositionManager::stopSharingPosition(const QString convId)
PositionManager::stopSharingPosition(QString accountId, const QString convId)
{
QString stopMsg;
stopMsg = "{\"type\":\"Stop\"}";
if (convId == "") {
sendPosition(stopMsg);
if (accountId == "") {
sendPosition(stopMsg, false);
stopPositionTimers();
positionShareConvIds_.clear();
set_positionShareConvIdsCount(positionShareConvIds_.size());
} else {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
if (convId == "") {
stopPositionTimers(accountId);
for (auto it = positionShareConvIds_.begin(); it != positionShareConvIds_.end();) {
if (it->first == accountId) {
sendStopMessage(accountId, it->second);
it = positionShareConvIds_.erase(it);
} else
++it;
}
} else {
sendStopMessage(accountId, convId);
auto iter = std::find(positionShareConvIds_.begin(),
positionShareConvIds_.end(),
PositionKey {accountId, convId});
if (iter != positionShareConvIds_.end()) {
positionShareConvIds_.remove(std::distance(positionShareConvIds_.begin(), iter));
}
}
}
if (!positionShareConvIds_.size())
countdownTimer_->stop();
set_positionShareConvIdsCount(positionShareConvIds_.size());
}
void
PositionManager::sendStopMessage(QString accountId, const QString convId)
{
QString stopMsg;
stopMsg = "{\"type\":\"Stop\"}";
if (accountId != "" && convId != "") {
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
Q_FOREACH (const QString& uri, convInfo.participantsUris()) {
if (lrcInstance_->getCurrentAccountInfo().profileInfo.uri != uri) {
lrcInstance_->getCurrentAccountInfo().contactModel->sendDhtMessage(uri,
@ -194,22 +245,68 @@ PositionManager::stopSharingPosition(const QString convId)
APPLICATION_GEO);
}
}
auto iter = std::find(positionShareConvIds_.begin(),
positionShareConvIds_.end(),
QPair<QString, QString> {lrcInstance_->get_currentAccountId(),
convId});
if (iter != positionShareConvIds_.end()) {
positionShareConvIds_.remove(std::distance(positionShareConvIds_.begin(), iter));
}
set_positionShareConvIdsCount(positionShareConvIds_.size());
}
}
void
PositionManager::setMapActive(bool state)
PositionManager::unPinMap(QString key)
{
set_isMapActive(state);
Q_EMIT isMapActiveChanged();
QMutexLocker lk(&mapStatusMutex_);
if (mapStatus_.find(key) != mapStatus_.end()) {
mapStatus_[key] = true;
Q_EMIT mapStatusChanged();
Q_EMIT unPinMapSignal(key);
} else {
qWarning() << "Error: Can't unpin a map that doesn't exist";
}
}
void
PositionManager::pinMap(QString key)
{
QMutexLocker lk(&mapStatusMutex_);
if (mapStatus_.find(key) != mapStatus_.end()) {
// map can be pined only if it's in the right account
if (key == lrcInstance_->get_currentAccountId()) {
mapStatus_[key] = false;
lk.unlock();
Q_EMIT mapStatusChanged();
Q_EMIT pinMapSignal(key);
} else {
lk.unlock();
setMapInactive(key);
}
}
}
void
PositionManager::setMapInactive(const QString key)
{
QMutexLocker lk(&mapStatusMutex_);
if (mapStatus_.find(key) != mapStatus_.end()) {
mapStatus_.remove(key);
Q_EMIT mapStatusChanged();
Q_EMIT closeMap(key);
if (!mapStatus_.size()) {
stopPositioning();
}
} else {
qWarning() << "Error: Can't set inactive a map that doesn't exists";
}
}
void
PositionManager::setMapActive(QString key)
{
if (mapStatus_.find(key) == mapStatus_.end()) {
mapStatus_.insert(key, false);
Q_EMIT mapStatusChanged();
// creation on QML
Q_EMIT openNewMap();
} else {
pinMap(key);
}
}
QString
@ -233,7 +330,9 @@ PositionManager::getAvatar(const QString& accountId, const QString& uri)
}
QVariantMap
PositionManager::parseJsonPosition(const QString& body, const QString& peerId)
PositionManager::parseJsonPosition(const QString& accountId,
const QString& peerId,
const QString& body)
{
QJsonDocument temp = QJsonDocument::fromJson(body.toUtf8());
QJsonObject jsonObject = temp.object();
@ -250,6 +349,7 @@ PositionManager::parseJsonPosition(const QString& body, const QString& peerId)
pos["time"] = i.value().toVariant();
pos["author"] = peerId;
pos["account"] = accountId;
}
return pos;
}
@ -257,17 +357,25 @@ PositionManager::parseJsonPosition(const QString& body, const QString& peerId)
void
PositionManager::startPositionTimers(int timeSharing)
{
set_timeSharingRemaining(timeSharing);
timerTimeLeftSharing_->start(1000);
timerStopSharing_->start(timeSharing);
mapTimerCountDown_[lrcInstance_->get_currentAccountId()] = timeSharing;
countdownUpdate();
countdownTimer_->start(1000);
}
void
PositionManager::stopPositionTimers()
PositionManager::stopPositionTimers(QString accountId)
{
set_timeSharingRemaining(0);
timerTimeLeftSharing_->stop();
timerStopSharing_->stop();
// reset all timers
if (accountId == nullptr) {
mapTimerCountDown_.clear();
} else {
auto it = mapTimerCountDown_.find(accountId);
if (it != mapTimerCountDown_.end()) {
mapTimerCountDown_.erase(it);
}
if (!mapTimerCountDown_.size())
countdownTimer_->stop();
}
}
void
@ -304,6 +412,151 @@ PositionManager::showNotification(const QString& accountId,
#endif
}
void
PositionManager::onNewConversation()
{
set_mapAutoOpening(true);
}
void
PositionManager::onNewAccount()
{
QMutexLocker lk(&mapStatusMutex_);
for (auto it = mapStatus_.begin(); it != mapStatus_.end();) {
if (it.value() == false) {
Q_EMIT closeMap(it.key());
it = mapStatus_.erase(it);
Q_EMIT mapStatusChanged();
} else {
it++;
}
}
}
bool
PositionManager::isNewMessageTriggersMap(bool endSharing,
const QString& uri,
const QString& accountId)
{
QMutexLocker lk(&mapStatusMutex_);
return !endSharing && (accountId == lrcInstance_->get_currentAccountId()) && mapAutoOpening_
&& (uri != lrcInstance_->getCurrentAccountInfo().profileInfo.uri)
&& (mapStatus_.find(accountId) == mapStatus_.end());
}
void
PositionManager::countdownUpdate()
{
// First removal of timers and shared position
auto end = std::find_if(mapTimerCountDown_.begin(),
mapTimerCountDown_.end(),
[](const auto& end) { return end == 0; });
if (end != mapTimerCountDown_.end()) {
Q_EMIT sendCountdownUpdate(end.key(), end.value());
stopSharingPosition(end.key());
}
// When removals are done, countdown can be updated
for (auto it = mapTimerCountDown_.begin(); it != mapTimerCountDown_.end(); it++) {
if (it.value() != 0) {
Q_EMIT sendCountdownUpdate(it.key(), it.value());
it.value() -= 1000;
}
}
}
void
PositionManager::addPositionToMap(PositionKey key, QVariantMap position)
{
// avatar only sent one time to qml, when a new position is added
position["avatar"] = getAvatar(key.first, key.second);
Q_EMIT positionShareAdded(position);
}
void
PositionManager::addPositionToMemory(PositionKey key, QVariantMap positionReceived)
{
// add the position to the list
auto obj = new PositionObject(positionReceived["lat"], positionReceived["long"], this);
objectListSharingUris_.insert(key, obj);
// information for qml
set_sharingUrisCount(objectListSharingUris_.size());
// watchdog
connect(obj,
&PositionObject::timeout,
this,
&PositionManager::onWatchdogTimeout,
Qt::DirectConnection);
auto& accountId = key.first;
auto& uri = key.second;
// Add position to the current map if needed)
addPositionToMap(key, positionReceived);
// show notification
if (accountId != "") {
QMutexLocker lk(&mapStatusMutex_);
if (mapStatus_.find(accountId) == mapStatus_.end()) {
auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri, accountId);
if (!convInfo.uid.isEmpty()) {
showNotification(accountId, convInfo.uid, uri);
}
}
}
}
void
PositionManager::updatePositionInMemory(PositionKey key, QVariantMap positionReceived)
{
auto it = objectListSharingUris_.find(key);
if (it != objectListSharingUris_.end()) {
if (it.value()) {
// reset watchdog
it.value()->resetWatchdog();
// update position
it.value()->updatePosition(positionReceived["lat"], positionReceived["long"]);
} else {
qWarning() << "error in PositionManager::updatePositionInMemory(), it.value() is null";
}
} else {
qWarning()
<< "Error: A position intented to be updated while not in objectListSharingUris_ ";
}
// update position on the map (if needed)
Q_EMIT positionShareUpdated(positionReceived);
}
void
PositionManager::removePositionFromMemory(PositionKey key, QVariantMap positionReceived)
{
// Remove
auto it = objectListSharingUris_.find(key);
if (it != objectListSharingUris_.end()) {
// free memory
it.value()->deleteLater();
// delete value
objectListSharingUris_.erase(it);
// update list count for qml
set_sharingUrisCount(objectListSharingUris_.size());
} else {
qWarning()
<< "Error: A position intented to be removed while not in objectListSharingUris_ ";
return;
}
// if needed, remove from map
Q_EMIT positionShareRemoved(key.second, positionReceived["account"].toString());
// close the map if you're not sharing and you don't receive position anymore
if (!positionShareConvIds_.length()
&& ((sharingUrisCount_ == 1
&& objectListSharingUris_.begin().key().second
== lrcInstance_->getCurrentAccountInfo().profileInfo.uri)
|| sharingUrisCount_ == 0)) {
setMapInactive(lrcInstance_->get_currentAccountId());
}
}
void
PositionManager::onPositionReceived(const QString& accountId,
const QString& peerId,
@ -311,106 +564,34 @@ PositionManager::onPositionReceived(const QString& accountId,
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();
// to know if the position received is from someone in the current conversation
bool isPeerIdInConv = (std::find(convParticipants.begin(), convParticipants.end(), peerId)
!= convParticipants.end());
// handlers variables
QVariantMap newPosition = parseJsonPosition(body, peerId);
auto getShareInfo = [&](bool update) -> QVariantMap {
QVariantMap shareInfo;
shareInfo["author"] = peerId;
if (!update) {
shareInfo["avatar"] = getAvatar(accountId, peerId);
}
shareInfo["long"] = newPosition["long"];
shareInfo["lat"] = newPosition["lat"];
return shareInfo;
};
auto endSharing = newPosition["type"] == "Stop";
// parse the position from json
QVariantMap positionReceived = parseJsonPosition(accountId, peerId, body);
auto key = QPair<QString, QString> {accountId, peerId};
// is it a message that notify an end of position sharing
auto endSharing = positionReceived["type"] == "Stop";
if (!endSharing) {
// open map on position reception
if (!isMapActive_ && mapAutoOpening_ && isPeerIdInConv
&& peerId != lrcInstance_->getCurrentAccountInfo().profileInfo.uri) {
set_isMapActive(true);
}
// key to identify the peer
auto key = PositionKey {accountId, peerId};
// check if the position exists in all shared positions, even if not visible to the screen
auto findPeerIdinAllPeers = objectListSharingUris_.find(key);
// open the map on position reception if needed
if (isNewMessageTriggersMap(endSharing, peerId, accountId)) {
setMapActive(accountId);
}
auto iter = std::find(currentConvSharingUris_.begin(), currentConvSharingUris_.end(), key);
if (iter == currentConvSharingUris_.end()) {
// New share
if (!endSharing) {
// list to save more information on position + watchdog
auto it = objectListSharingUris_.find(key);
auto isNewSharing = it == objectListSharingUris_.end();
if (isNewSharing) {
auto obj = new PositionObject(newPosition["lat"], newPosition["long"], this);
objectListSharingUris_.insert(key, obj);
set_sharingUrisCount(objectListSharingUris_.size());
connect(obj,
&PositionObject::timeout,
this,
&PositionManager::onWatchdogTimeout,
Qt::DirectConnection);
}
// if the position already exists
if (findPeerIdinAllPeers != objectListSharingUris_.end()) {
if (endSharing)
removePositionFromMemory(key, positionReceived);
else
updatePositionInMemory(key, positionReceived);
if (isPeerIdInConv) {
currentConvSharingUris_.insert(key);
Q_EMIT positionShareAdded(getShareInfo(false));
} else if (isNewSharing && accountId != "") {
auto& convInfo = lrcInstance_->getConversationFromPeerUri(peerId, accountId);
if (!convInfo.uid.isEmpty()) {
showNotification(accountId, convInfo.uid, peerId);
}
}
// stop sharing position
} else {
auto it = objectListSharingUris_.find(key);
if (it != objectListSharingUris_.end()) {
it.value()->deleteLater();
objectListSharingUris_.erase(it);
set_sharingUrisCount(objectListSharingUris_.size());
}
}
} else {
// Update/remove existing
if (endSharing) {
// Remove
auto it = objectListSharingUris_.find(key);
if (it != objectListSharingUris_.end()) {
it.value()->deleteLater();
objectListSharingUris_.erase(it);
set_sharingUrisCount(objectListSharingUris_.size());
}
if (isPeerIdInConv) {
currentConvSharingUris_.remove(key);
Q_EMIT positionShareRemoved(peerId);
// close the map if you're not sharing and you don't receive position anymore
if (!positionShareConvIds_.length()
&& ((sharingUrisCount_ == 1
&& objectListSharingUris_.contains(QPair<QString, QString> {
"", lrcInstance_->getCurrentAccountInfo().profileInfo.uri}))
|| sharingUrisCount_ == 0)) {
set_isMapActive(false);
}
}
} else {
// Update
if (isPeerIdInConv)
Q_EMIT positionShareUpdated(getShareInfo(true));
// reset watchdog
auto it = objectListSharingUris_.find(key);
if (it != objectListSharingUris_.end()) {
it.value()->resetWatchdog();
}
}
// It is the first time a position is received from this peer
addPositionToMemory(key, positionReceived);
}
}

View file

@ -24,19 +24,21 @@
#include "positionobject.h"
#include "systemtray.h"
#include <QMutex>
#include <QObject>
#include <QString>
class PositionManager : public QmlAdapterBase
{
Q_OBJECT
QML_RO_PROPERTY(bool, isMapActive)
QML_RO_PROPERTY(int, timeSharingRemaining)
// map of elements : map key and isUnpin
QML_PROPERTY(QVariantMap, mapStatus)
QML_PROPERTY(bool, mapAutoOpening)
QML_PROPERTY(int, positionShareConvIdsCount)
QML_PROPERTY(int, sharingUrisCount)
QML_PROPERTY(bool, mapAutoOpening)
public:
explicit PositionManager(SystemTray* systemTray,
explicit PositionManager(AppSettingsManager* settingsManager,
SystemTray* systemTray,
LRCInstance* instance,
QObject* parent = nullptr);
~PositionManager() = default;
@ -45,30 +47,49 @@ Q_SIGNALS:
void positioningError(const QString error);
void positionShareAdded(const QVariantMap& shareInfo);
void positionShareUpdated(const QVariantMap& posInfo);
void positionShareRemoved(const QString& uri);
void positionShareRemoved(const QString& uri, const QString& accountId);
void openNewMap();
void closeMap(const QString& key);
void pinMapSignal(const QString& key);
void unPinMapSignal(const QString& key);
void localPositionReceived(const QString& accountId, const QString& peerId, const QString& body);
void makeVisibleSharingButton(const QString& accountId);
void sendCountdownUpdate(const QString& accountId, const int remainingTime);
protected:
void safeInit() override;
QString getAvatar(const QString& accountId, const QString& peerId);
QVariantMap parseJsonPosition(const QString& body, const QString& peerId);
QVariantMap parseJsonPosition(const QString& accountId,
const QString& peerId,
const QString& body);
void addPositionToMap(PositionKey key, QVariantMap position);
void addPositionToMemory(PositionKey key, QVariantMap positionReceived);
void updatePositionInMemory(PositionKey key, QVariantMap positionReceived);
void removePositionFromMemory(PositionKey key, QVariantMap positionReceived);
void positionWatchDog();
void startPositionTimers(int timeSharing);
void stopPositionTimers();
void stopPositionTimers(QString accountId = {});
bool isNewMessageTriggersMap(bool endSharing, const QString& uri, const QString& accountId);
void countdownUpdate();
void sendStopMessage(QString accountId = "", const QString convId = "");
Q_INVOKABLE void connectAccountModel();
Q_INVOKABLE void setMapActive(bool state);
Q_INVOKABLE void sharePosition(int maximumTime);
Q_INVOKABLE void stopSharingPosition(const QString convId = "");
Q_INVOKABLE void pinMap(QString key);
Q_INVOKABLE void unPinMap(QString key);
Q_INVOKABLE void setMapActive(QString key);
Q_INVOKABLE void setMapInactive(const QString key);
Q_INVOKABLE void sharePosition(int maximumTime, QString accountId, QString convId);
Q_INVOKABLE void stopSharingPosition(QString accountId = "", const QString convId = "");
Q_INVOKABLE void startPositioning();
Q_INVOKABLE void stopPositioning();
Q_INVOKABLE QString getSelectedConvId();
Q_INVOKABLE bool isPositionSharedToConv(const QString& convUri);
Q_INVOKABLE bool isConvSharingPosition(const QString& convUri);
Q_INVOKABLE bool isPositionSharedToConv(const QString& accountId, const QString& convUid);
Q_INVOKABLE bool isConvSharingPosition(const QString& accountId, const QString& convUri);
Q_INVOKABLE void loadPreviousLocations();
Q_INVOKABLE void loadPreviousLocations(QString& accountId);
Q_INVOKABLE QString getmapTitle(QString& accountId, QString convId = "");
private Q_SLOTS:
void onPositionErrorReceived(const QString error);
@ -77,16 +98,21 @@ private Q_SLOTS:
const QString& body,
const uint64_t& timestamp,
const QString& daemonId);
void sendPosition(const QString& body);
void sendPosition(const QString& body, bool triggersLocalPosition = true);
void onWatchdogTimeout();
void showNotification(const QString& accountId, const QString& convId, const QString& from);
void onNewConversation();
void onNewAccount();
private:
SystemTray* systemTray_;
std::unique_ptr<Positioning> localPositioning_;
QTimer* timerTimeLeftSharing_ = nullptr;
QTimer* timerStopSharing_ = nullptr;
QSet<QPair<QString, QString>> currentConvSharingUris_;
QMap<QPair<QString, QString>, PositionObject*> objectListSharingUris_;
QList<QPair<QString, QString>> positionShareConvIds_;
QMap<QString, int> mapTimerCountDown_;
QTimer* countdownTimer_ = nullptr;
// map of all shared position by peers
QMap<PositionKey, PositionObject*> objectListSharingUris_;
// list of all the peers the user is sharing position to
QList<PositionKey> positionShareConvIds_;
QMutex mapStatusMutex_;
AppSettingsManager* settingsManager_;
};

View file

@ -2,7 +2,7 @@
PositionObject::PositionObject(QVariant latitude, QVariant longitude, QObject* parent)
: QObject(parent)
, resetTime(20000)
, resetTime(40000)
, longitude_(longitude)
, latitude_(latitude)
@ -28,3 +28,10 @@ PositionObject::getLatitude()
{
return latitude_;
}
void
PositionObject::updatePosition(QVariant latitude, QVariant longitude)
{
longitude_ = longitude;
latitude_ = latitude;
}

View file

@ -20,6 +20,8 @@ public:
QVariant getLongitude();
QVariant getLatitude();
void updatePosition(QVariant latitude, QVariant longitude);
private:
QVariant latitude_;
QVariant longitude_;

View file

@ -112,7 +112,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(systemTray, lrcInstance, parent);
auto positionManager = new PositionManager(settingsManager, systemTray, lrcInstance, parent);
auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent);
auto avAdapter = new AvAdapter(lrcInstance, parent);
auto contactAdapter = new ContactAdapter(lrcInstance, parent);

View file

@ -63,6 +63,16 @@ Rectangle {
itemWidth: preferredColumnWidth
}
// location sharing setting panel
LocationSharingSettings {
Layout.fillWidth: true
Layout.topMargin: JamiTheme.preferredMarginSize
Layout.leftMargin: JamiTheme.preferredMarginSize
Layout.rightMargin: JamiTheme.preferredMarginSize
itemWidth: preferredColumnWidth
}
// file transfer setting panel
FileTransferSettings {
id: fileTransferSettings

View file

@ -0,0 +1,150 @@
/*
* 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/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Enums 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
ColumnLayout {
id: root
property int itemWidth
Label {
Layout.fillWidth: true
text: JamiStrings.locationSharingLabel
font.pointSize: JamiTheme.headerFontSize
font.kerning: true
color: JamiTheme.textColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
ToggleSwitch {
id: isTimeLimit
visible: WITH_WEBENGINE
Layout.fillWidth: true
Layout.leftMargin: JamiTheme.preferredMarginSize
checked: UtilsAdapter.getAppValue(Settings.PositionShareLimit)
labelText: JamiStrings.positionShareLimit
fontPointSize: JamiTheme.settingsFontSize
tooltipText: JamiStrings.positionShareLimit
onSwitchToggled: {
positionSharingLimitation = !UtilsAdapter.getAppValue(Settings.PositionShareLimit)
UtilsAdapter.setAppValue(Settings.PositionShareLimit,
positionSharingLimitation)
}
property bool positionSharingLimitation: UtilsAdapter.getAppValue(Settings.PositionShareLimit)
}
RowLayout {
id: timeSharingLocation
Layout.fillWidth: true
Layout.preferredHeight: JamiTheme.preferredFieldHeight
Layout.leftMargin: JamiTheme.preferredMarginSize
visible: isTimeLimit.positionSharingLimitation
function standartCountdown(minutes) {
var hour = Math.floor(minutes / 60)
var min = minutes % 60
if (hour) {
if (min)
return qsTr("%1h%2min").arg(hour).arg(min)
else
return qsTr("%1h").arg(hour)
}
return qsTr("%1min").arg(min)
}
Text {
Layout.fillWidth: true
Layout.rightMargin: JamiTheme.preferredMarginSize / 2
color: JamiTheme.textColor
text: JamiStrings.positionShareDuration
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
Text {
id: timeSharingLocationValueLabel
Layout.alignment: Qt.AlignRight
Layout.fillHeight: true
Layout.fillWidth: true
Layout.rightMargin: JamiTheme.preferredMarginSize / 2
color: JamiTheme.textColor
text: timeSharingLocation.standartCountdown(UtilsAdapter.getAppValue(Settings.PositionShareDuration))
font.pointSize: JamiTheme.settingsFontSize
font.kerning: true
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
}
Slider {
id: timeSharingSlider
Layout.maximumWidth: itemWidth
Layout.alignment: Qt.AlignRight
Layout.fillWidth: true
Layout.fillHeight: true
value: Math.log(UtilsAdapter.getAppValue(Settings.PositionShareDuration))
from: 0.5
to: Math.log(600)
stepSize: 0.05
onMoved: {
timeSharingLocationValueLabel.text = timeSharingLocation.standartCountdown(Math.floor(Math.exp(value)))
UtilsAdapter.setAppValue(Settings.PositionShareDuration, Math.floor(Math.exp(value)))
}
MaterialToolTip {
id: toolTip
text: JamiStrings.positionShareDuration
visible: parent.hovered
delay: Qt.styleHints.mousePressAndHoldInterval
}
}
}
}

View file

@ -15,6 +15,7 @@
* 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
@ -23,373 +24,212 @@ import QtWebEngine
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Enums 1.1
import net.jami.Constants 1.1
import "../../commoncomponents"
Rectangle {
id: mapPopup
Item {
id: root
x: xPos
y: yPos
width: isFullScreen ? root.width : windowSize
height: isMinimised
? buttonOverlay.height + buttonsChoseSharing.height + 30
: isFullScreen ? root.height - yPos : windowSize
property bool isUnpin: false
property real maxWidth
property real maxHeight
property string attachedAccountId
property string currentAccountId: CurrentAccount.id
property string currentConvId: CurrentConversation.id
property bool isSharing: (PositionManager.positionShareConvIdsCount !== 0)
property bool isSharingToCurrentConversation
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
function closeMapPosition() {
root.destroy()
}
WebEngineView {
id: webView
Connections {
target: PositionManager
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.positionShareConvIdsCount !== 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()" );
}
function onPinMapSignal(key) {
if (key === attachedAccountId) {
isUnpin = false
mapObject.state = "pin"
windowUnpin.close()
}
}
Component.onCompleted: {
loadHtml(UtilsAdapter.qStringFromFile(mapHtml), mapHtml)
loadScripts()
function onCloseMap(key) {
if (key === attachedAccountId )
closeMapPosition()
}
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()
//load locations that were received before this conversation was opened
PositionManager.loadPreviousLocations();
function onUnPinMapSignal(key) {
if (key === attachedAccountId ) {
isUnpin = true
mapObject.state = "unpin"
windowUnpin.show()
}
}
}
ColumnLayout {
id: buttonsChoseSharing
Window {
id: windowUnpin
anchors.horizontalCenter: mapPopup.horizontalCenter
anchors.margins: 10
anchors.bottom: mapPopup.bottom
width: parentPin.width
height: parentPin.height
visible: false
title: PositionManager.getmapTitle(attachedAccountId)
property bool shortSharing: true
Item {
id: parentUnPin
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)
}
}
width: mapObject.width
height: mapObject.height
}
RowLayout {
id: sharePositionLayout
Layout.alignment: Qt.AlignHCenter
onClosing: {
if (isUnpin) {
PositionManager.setMapInactive(attachedAccountId)
}
}
}
MaterialButton {
id: sharePositionButton
Item {
id: parentPin
preferredWidth: text.contentWidth
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
visible: ! PositionManager.isPositionSharedToConv(PositionManager.getSelectedConvId())
text: JamiStrings.shareLocation
color: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedBlue
hoveredColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedBlueHovered
pressedColor: isError
? JamiTheme.buttonTintedGreyInactive
: 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
width: mapObject.width
height: mapObject.height
Rectangle {
id: mapObject
x: xPos
y: yPos
width: root.isUnpin
? windowUnpin.width
: isFullScreen ? root.maxWidth : windowSize
height: root.isUnpin
? windowUnpin.height
: isFullScreen ? root.maxHeight - yPos : windowSize
property bool isFullScreen: false
property real windowSize: windowPreferedSize > JamiTheme.minimumMapWidth
? windowPreferedSize
: JamiTheme.minimumMapWidth
property real windowPreferedSize: root.maxWidth > root.maxHeight
? root.maxHeight / 3
: root.maxWidth / 3
property real xPos: 0
property real yPos: root.isUnpin ? 0 : JamiTheme.chatViewHeaderPreferredHeight
states: [ State {
name: "unpin"
ParentChange { target: mapObject; parent: parentUnPin; x:0; y:0 }
},
State {
name: "pin"
ParentChange { target: mapObject; parent: parentPin; x:xPos; y:JamiTheme.chatViewHeaderPreferredHeight }
}
]
property alias webView: webView
onClicked: {
if (!isError) {
if( buttonsChoseSharing.shortSharing)
PositionManager.sharePosition(10 * 60 * 1000);
else
PositionManager.sharePosition(60 * 60 * 1000);
visible = false
WebEngineView {
id: webView
layer.enabled: !isFullScreen
layer.effect: OpacityMask {
maskSource:
Rectangle {
width: webView.width
height: webView.height
radius: 10
}
}
onHoveredChanged: {
isHovered = !isHovered
}
width: parent.width
height: parent.height
MaterialToolTip {
visible: sharePositionButton.isHovered
&& sharePositionButton.isError && (sharePositionButton.positioningError !== "default")
x: 0
y: 0
text: sharePositionButton.errorString(sharePositionButton.positioningError)
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;
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 onPositioningError (err) {
sharePositionButton.positioningError = err;
}
}
}
MaterialButton {
id: stopSharingPositionButton
preferredWidth: text.contentWidth
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
visible: webView.isSharing
text: JamiStrings.stopSharingLocation
color: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRed
hoveredColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRedHovered
pressedColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRedPressed
Layout.alignment: Qt.AlignHCenter
property bool isHovered: false
property string positioningError
property bool isError: positioningError.length
onClicked: {
if (!isError) {
if (PositionManager.positionShareConvIdsCount >= 2) {
stopSharingPositionPopup.open()
} else {
PositionManager.stopSharingPosition();
sharePositionButton.visible = true
function onPositionShareAdded(shareInfo) {
if(webView.isLoaded) {
if (shareInfo.account === attachedAccountId) {
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) {
if (shareInfo.account === attachedAccountId) {
var curLong = shareInfo.long
var curLat = shareInfo.lat
webView.runJavaScript("updatePosition([" + curLong + "," + curLat + "], '" + shareInfo.author + "' )" );
}
}
}
function onPositionShareRemoved(author, accountId) {
if(webView.isLoaded) {
if (accountId === attachedAccountId) {
webView.runJavaScript("removePosition( '" + author + "' )" );
webView.runJavaScript("zoomTolayersExtent()" );
}
}
}
}
}
}
}
StopSharingPositionPopup {
id: stopSharingPositionPopup
property alias shareButtonVisibility: sharePositionButton.visible
}
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
toolTipText: JamiStrings.centerMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.share_location_svg
onClicked: {
webView.runJavaScript("zoomTolayersExtent()" );
Component.onCompleted: {
loadHtml(UtilsAdapter.qStringFromFile(mapHtml), mapHtml)
loadScripts()
}
}
PushButton {
id: btnMove
toolTipText: JamiStrings.dragMapTooltip
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: btnminimize
toolTipText: isMinimised
? JamiStrings.extendMapTooltip
: JamiStrings.minimizeMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: isMinimised
? JamiResources.close_fullscreen_24dp_svg
: JamiResources.minimize_svg
onClicked: {
isMinimised = !isMinimised
isFullScreen = false;
}
}
PushButton {
id: btnmaximise
toolTipText: isFullScreen
? JamiStrings.reduceMapTooltip
: JamiStrings.maximizeMapTooltip
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
onLoadingChanged: function (loadingInfo) {
if (loadingInfo.status === WebEngineView.LoadSucceededStatus) {
attachedAccountId = CurrentAccount.id
runJavaScript(UtilsAdapter.getStyleSheet("olcss",UtilsAdapter.qStringFromFile(olCss)))
webView.isLoaded = true
runJavaScript("setMapView([" + 0 + ","+ 0 + "], " + 1 + " );" );
PositionManager.startPositioning()
//load locations that were received before this conversation was opened
PositionManager.loadPreviousLocations(attachedAccountId);
}
isFullScreen = !isFullScreen
isMinimised = false;
}
}
PushButton {
id: btnClose
MapPositionSharingControl {}
toolTipText: JamiStrings.closeMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.round_close_24dp_svg
MapPositionOverlay {}
onClicked: {
PositionManager.stopPositioning();
PositionManager.setMapActive(false);
PositionManager.mapAutoOpening = false;
StopSharingPositionPopup {
id: stopSharingPositionPopup
}
property alias attachedAccountId: root.attachedAccountId
}
}
}

View file

@ -0,0 +1,125 @@
/*
* 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.Layouts
import net.jami.Constants 1.1
import net.jami.Adapters 1.1
import "../../commoncomponents"
Rectangle {
id: root
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: btnUnpin
toolTipText: !isUnpin ? JamiStrings.unpin : JamiStrings.pinWindow
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.unpin_svg
onClicked: {
if (!isUnpin) {
PositionManager.unPinMap(attachedAccountId)
} else {
PositionManager.pinMap(attachedAccountId)
}
}
}
PushButton {
id: btnCenter
toolTipText: JamiStrings.centerMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.share_location_svg
onClicked: {
webView.runJavaScript("zoomTolayersExtent()" );
}
}
PushButton {
id: btnMove
toolTipText: JamiStrings.dragMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.move_svg
visible: !isUnpin
MouseArea {
anchors.fill: parent
drag.target: mapObject
drag.minimumX: 0
drag.maximumX: maxWidth - mapObject.maxWidth
drag.minimumY: 0
drag.maximumY: maxHeight - mapObject.maxHeight
}
}
PushButton {
id: btnMaximise
visible: !isUnpin
toolTipText: mapObject.isFullScreen
? JamiStrings.reduceMapTooltip
: JamiStrings.maximizeMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: mapObject.isFullScreen? JamiResources.close_fullscreen_24dp_svg : JamiResources.open_in_full_24dp_svg
onClicked: {
if (!mapObject.isFullScreen) {
mapObject.x = mapObject.xPos
mapObject.y = mapObject.yPos
}
mapObject.isFullScreen = !mapObject.isFullScreen
}
}
PushButton {
id: btnClose
toolTipText: JamiStrings.closeMapTooltip
imageColor: JamiTheme.mapButtonColor
normalColor: JamiTheme.transparentColor
source: JamiResources.round_close_24dp_svg
visible: !isUnpin
onClicked: {
PositionManager.setMapInactive(attachedAccountId)
PositionManager.mapAutoOpening = false
}
}
}
}

View file

@ -0,0 +1,203 @@
/*
* 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.Layouts
import net.jami.Constants 1.1
import net.jami.Adapters 1.1
import net.jami.Enums 1.1
import "../../commoncomponents"
ColumnLayout {
id: root
anchors.horizontalCenter: mapObject.horizontalCenter
anchors.margins: 10
anchors.bottom: mapObject.bottom
RowLayout {
Layout.alignment: Qt.AlignHCenter
Rectangle {
radius: 10
Layout.preferredWidth: textTimer.width + 15
Layout.preferredHeight: textTimer.height + 15
color: JamiTheme.mapButtonsOverlayColor
visible: textTimer.remainingTimeMs === 0
? false
: isUnpin
? isSharing
: isSharingToCurrentConversation
Text {
id: textTimer
anchors.centerIn: parent
color: JamiTheme.mapButtonColor
text: standartCountdown(Math.floor(remainingTimeMs / 1000))
function standartCountdown(seconds) {
var minutes = Math.floor(seconds / 60);
var hour = Math.floor(minutes / 60)
minutes = minutes % 60
var sec = seconds % 60
if (hour) {
if (minutes)
return qsTr("%1h%2min").arg(hour).arg(minutes)
else
return qsTr("%1h").arg(hour)
}
if (minutes) {
if (sec)
return qsTr("%1m%2sec").arg(minutes).arg(sec)
else
return qsTr("%1m").arg(minutes)
}
return qsTr("%1sec").arg(sec)
}
property int remainingTimeMs: 0
Connections {
target: PositionManager
function onSendCountdownUpdate(accountId, remainingTime) {
if (accountId === attachedAccountId) {
textTimer.remainingTimeMs = remainingTime
}
}
}
}
}
}
RowLayout {
id: sharePositionLayout
Layout.alignment: Qt.AlignHCenter
MaterialButton {
id: sharePositionButton
preferredWidth: text.contentWidth
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
visible: !isSharingToCurrentConversation && !isUnpin
text: JamiStrings.shareLocation
color: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedBlue
hoveredColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedBlueHovered
pressedColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedBluePressed
Layout.alignment: Qt.AlignHCenter
property bool isHovered: false
property string positioningError: "default"
property bool isError: positioningError.length
property int positionShareConvIdsCount: PositionManager.positionShareConvIdsCount
property string currentConvId: CurrentConversation.id
property bool isMapUnpin: isUnpin
function errorString(posError) {
if (posError === "locationServicesError")
return JamiStrings.locationServicesError
return JamiStrings.locationServicesClosedError
}
onPositionShareConvIdsCountChanged: {
isSharingToCurrentConversation = PositionManager.isPositionSharedToConv(attachedAccountId, currentConvId)
}
onCurrentConvIdChanged: {
isSharingToCurrentConversation = PositionManager.isPositionSharedToConv(attachedAccountId, currentConvId)
}
onIsMapUnpinChanged: {
isSharingToCurrentConversation = PositionManager.isPositionSharedToConv(attachedAccountId, currentConvId)
}
onClicked: {
var sharingDuration = 60 * 1000 * UtilsAdapter.getAppValue(Settings.PositionShareDuration)
if (!isError && !isUnpin) {
PositionManager.sharePosition(sharingDuration, attachedAccountId, currentConvId);
}
webView.runJavaScript("zoomTolayersExtent()" );
}
MaterialToolTip {
property bool isSharingPossible: !(sharePositionButton.isError && (sharePositionButton.positioningError !== "default"))
visible: sharePositionButton.hovered
text: isSharingPossible
? JamiStrings.shareLocationToolTip.arg(PositionManager.getmapTitle(attachedAccountId, currentConvId))
: sharePositionButton.errorString(sharePositionButton.positioningError)
}
Connections {
target: PositionManager
function onPositioningError (err) {
sharePositionButton.positioningError = err;
}
}
}
MaterialButton {
id: stopSharingPositionButton
preferredWidth: text.contentWidth
textLeftPadding: JamiTheme.buttontextPadding
textRightPadding: JamiTheme.buttontextPadding
primary: true
visible: isSharing
text: stopAllSharing
? JamiStrings.shortStopAllSharings
: JamiStrings.stopSharingLocation
color: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRed
hoveredColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRedHovered
pressedColor: isError
? JamiTheme.buttonTintedGreyInactive
: JamiTheme.buttonTintedRedPressed
Layout.alignment: Qt.AlignHCenter
toolTipText: stopAllSharing
? isUnpin
? JamiStrings.unpinStopSharingTooltip
: JamiStrings.stopAllSharings
: JamiStrings.stopSharingSeveralConversationTooltip
property bool isHovered: false
property string positioningError
property bool isError: positioningError.length
property bool stopAllSharing: !(PositionManager.positionShareConvIdsCount >= 2 && !isUnpin && isSharingToCurrentConversation)
onClicked: {
if (!isError) {
if (stopAllSharing) {
PositionManager.stopSharingPosition();
} else {
stopSharingPositionPopup.open()
}
}
}
}
}
}

View file

@ -19,12 +19,11 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import net.jami.Models 1.1
import net.jami.Adapters 1.1
import net.jami.Constants 1.1
import Qt5Compat.GraphicalEffects
import "../../commoncomponents"
@ -101,11 +100,10 @@ Popup {
color: JamiTheme.buttonTintedBlue
hoveredColor: JamiTheme.buttonTintedBlueHovered
pressedColor: JamiTheme.buttonTintedBluePressed
text: JamiStrings.stopConvSharing
text: JamiStrings.stopConvSharing.arg(PositionManager.getmapTitle(attachedAccountId, CurrentConversation.id))
onClicked: {
PositionManager.stopSharingPosition(PositionManager.getSelectedConvId())
shareButtonVisibility = true
PositionManager.stopSharingPosition(attachedAccountId, CurrentConversation.id)
root.close()
}
}
@ -123,12 +121,10 @@ Popup {
onClicked: {
PositionManager.stopSharingPosition()
shareButtonVisibility = true
root.close()
}
}
}
}
}

View file

@ -61,7 +61,7 @@ var proj = new ol.proj.Projection({
extent: extent
})
function setSource (coordos, authorI,imageI) {
function setSource (coordos, avatar) {
var coord = ol.proj.fromLonLat(coordos)
var pointFeature = new ol.Feature({
geometry: new ol.geom.Point(coord),
@ -69,16 +69,16 @@ function setSource (coordos, authorI,imageI) {
})
var preStyle = new ol.style.Icon({
src: "data:image/png;base64," + imageI})
src: "data:image/png;base64," + avatar})
//resize the image to 40 px
//resize the image to 35 px
var image = preStyle.getImage()
if (!image.width) {
image.addEventListener('load', function () {
preStyle.setScale([40 / image.width, 40 / image.height])
preStyle.setScale([35 / image.width, 35 / image.height])
})
} else {
preStyle.setScale([40 / image.width, 40 / image.height])
preStyle.setScale([35 / image.width, 35 / image.height])
}
var iconStyle = new ol.style.Style({
@ -93,18 +93,24 @@ function setSource (coordos, authorI,imageI) {
return vectorSource
}
function newPosition (coordos, authorI, image) {
vectorSource = setSource(coordos, authorI, image)
function newPosition (coordos, authorUri, avatar) {
var layerArray = map.getLayers().getArray();
for (var i = 0; i < layerArray.length; i++ ){
if(layerArray[i].layer_type === authorUri) {
return
}
}
vectorSource = setSource(coordos, avatar)
var iconLayer = new ol.layer.Vector({source: vectorSource})
iconLayer.layer_type = authorI
iconLayer.layer_type = authorUri
map.addLayer(iconLayer)
}
function updatePosition (coordos, authorI) {
function updatePosition (coordos, authorUri) {
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) {
if(layerArray[i].layer_type === authorUri) {
layerArray[i].getSource().getFeatures()[0].getGeometry().setCoordinates(coord)
return
}
@ -123,10 +129,10 @@ function zoomTolayersExtent() {
padding: [80 ,80 ,80 ,80]})
}
function removePosition (authorI) {
function removePosition (authorUri) {
var layerArray = map.getLayers().getArray();
for (var i = 0; i < layerArray.length; i++ ){
if(layerArray[i].layer_type === authorI) {
if(layerArray[i].layer_type === authorUri) {
map.removeLayer(layerArray[i])
return
}

View file

@ -30,6 +30,7 @@
typedef QMap<QString, QString> MapStringString;
typedef QMap<QString, int> MapStringInt;
typedef QMap<QString, double> MapStringDouble;
typedef QMap<QPair<QString, QString>, bool> MapPairStrStrBool;
typedef QVector<int> VectorInt;
typedef QVector<uint> VectorUInt;
typedef QVector<qulonglong> VectorULongLong;
@ -41,6 +42,8 @@ typedef QMap<QString, QMap<QString, QStringList>> MapStringMapStringStringList;
typedef QMap<QString, QStringList> MapStringStringList;
typedef QVector<QByteArray> VectorVectorByte;
typedef uint64_t DataTransferId;
// accountId, conversationId
typedef QPair<QString, QString> PositionKey;
constexpr static const char* TRUE_STR = "true";
constexpr static const char* TEXT_PLAIN = "text/plain";