diff --git a/resources/artwork/icons/im-user-away.svg b/resources/artwork/icons/im-user-away.svg
new file mode 100644
index 00000000..90703f4d
--- /dev/null
+++ b/resources/artwork/icons/im-user-away.svg
@@ -0,0 +1,6 @@
+
diff --git a/resources/artwork/icons/im-user-busy.svg b/resources/artwork/icons/im-user-busy.svg
new file mode 100644
index 00000000..55d6c422
--- /dev/null
+++ b/resources/artwork/icons/im-user-busy.svg
@@ -0,0 +1,7 @@
+
diff --git a/resources/artwork/icons/im-user-online.svg b/resources/artwork/icons/im-user-online.svg
new file mode 100644
index 00000000..ea50dd84
--- /dev/null
+++ b/resources/artwork/icons/im-user-online.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 86baa5b0..696e9b99 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -369,10 +369,12 @@ qt_add_qml_module(gonnect
ui/components/popups/AudioDeviceMenu.qml
ui/components/popups/BurgerMenu.qml
ui/components/popups/DialInInfo.qml
+ ui/components/popups/EditStatusText.qml
ui/components/popups/FirstAid.qml
ui/components/popups/HistoryListContextMenu.qml
ui/components/popups/JitsiHistoryListContextMenu.qml
ui/components/popups/Menu.qml
+ ui/components/popups/OwnAvatarContextMenu.qml
ui/components/popups/SearchResultPopup.qml
ui/components/popups/VideoDeviceMenu.qml
ui/components/popups/StreamingLightPopup.qml
@@ -410,6 +412,7 @@ qt_add_qml_module(gonnect
ui/components/HistoryList.qml
ui/components/MainTabBar.qml
ui/components/ParticipantsList.qml
+ ui/components/PresenceStatusIndicator.qml
ui/components/RTTDisplay.qml
ui/components/SettingsPage.qml
ui/components/TogglerList.qml
@@ -619,6 +622,10 @@ qt_add_qml_module(gonnect
platform/SearchProvider.h
${PLATFORM_SOURCES}
+ presence/GlobalStateAggregator.h
+ presence/GlobalStateAggregator.cpp
+ presence/PresenceState.h
+
rtt/RTTProvider.h
rtt/RTTProvider.cpp
rtt/RTTMessage.h
@@ -674,6 +681,8 @@ qt_add_qml_module(gonnect
ui/RandomRoomNameGenerator.cpp
ui/ParticipantsModel.h
ui/ParticipantsModel.cpp
+ ui/PersonCoinProvider.h
+ ui/PersonCoinProvider.cpp
ui/SearchListModel.h
ui/SearchListModel.cpp
ui/SearchListProxyModel.h
@@ -792,6 +801,7 @@ target_include_directories(gonnect
calendar
contacts
conference
+ presence
ui
ui/chat
usb
diff --git a/src/contacts/akonadi/CMakeLists.txt b/src/contacts/akonadi/CMakeLists.txt
index 6ece5cf3..c32b41c1 100644
--- a/src/contacts/akonadi/CMakeLists.txt
+++ b/src/contacts/akonadi/CMakeLists.txt
@@ -20,6 +20,7 @@ if(ENABLE_AKONADI)
target_include_directories(AkonadiAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src/sip
${PROJECT_SOURCE_DIR}/src
diff --git a/src/contacts/carddav/CMakeLists.txt b/src/contacts/carddav/CMakeLists.txt
index a83cd9f5..47a47215 100644
--- a/src/contacts/carddav/CMakeLists.txt
+++ b/src/contacts/carddav/CMakeLists.txt
@@ -16,6 +16,7 @@ if(ENABLE_DAV)
target_include_directories(CardDAVAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/dbus/portal
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src/sip
diff --git a/src/contacts/csv/CMakeLists.txt b/src/contacts/csv/CMakeLists.txt
index 5da8880d..f6191e3f 100644
--- a/src/contacts/csv/CMakeLists.txt
+++ b/src/contacts/csv/CMakeLists.txt
@@ -11,6 +11,7 @@ if(ENABLE_CSV)
target_include_directories(CSVAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src/sip
${PROJECT_SOURCE_DIR}/src
diff --git a/src/contacts/eds/CMakeLists.txt b/src/contacts/eds/CMakeLists.txt
index d8062f55..f4793d4a 100644
--- a/src/contacts/eds/CMakeLists.txt
+++ b/src/contacts/eds/CMakeLists.txt
@@ -14,6 +14,7 @@ if(ENABLE_EDS)
target_include_directories(EDSAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/dbus/portal
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src/sip
diff --git a/src/contacts/ldap/CMakeLists.txt b/src/contacts/ldap/CMakeLists.txt
index 5d9079fe..f8cf65b4 100644
--- a/src/contacts/ldap/CMakeLists.txt
+++ b/src/contacts/ldap/CMakeLists.txt
@@ -29,6 +29,7 @@ if(ENABLE_LDAP)
target_include_directories(LDAPAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/dbus/portal
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src/sip
diff --git a/src/contacts/msgraph/CMakeLists.txt b/src/contacts/msgraph/CMakeLists.txt
index 41f1eb09..5b725bda 100644
--- a/src/contacts/msgraph/CMakeLists.txt
+++ b/src/contacts/msgraph/CMakeLists.txt
@@ -11,6 +11,7 @@ if(ENABLE_MSGRAPH)
target_include_directories(MSGraphAddressBookFactory
PRIVATE
${PROJECT_SOURCE_DIR}/src/contacts
+ ${PROJECT_SOURCE_DIR}/src/presence
${PROJECT_SOURCE_DIR}/src/ui
${PROJECT_SOURCE_DIR}/src
${PROJECT_SOURCE_DIR}
diff --git a/src/main.cpp b/src/main.cpp
index 31bb8844..999fedfd 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -3,6 +3,7 @@
#include
#include "Application.h"
#include "GlobalInfo.h"
+#include "PersonCoinProvider.h"
#ifdef Q_OS_LINUX
# include
@@ -116,6 +117,8 @@ int main(int argc, char *argv[])
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
[]() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
+
+ engine.addImageProvider(QLatin1String("personcoin"), new PersonCoinProvider);
engine.loadFromModule("base", "Main");
const auto &objs = engine.rootObjects();
diff --git a/src/presence/GlobalStateAggregator.cpp b/src/presence/GlobalStateAggregator.cpp
new file mode 100644
index 00000000..a844190d
--- /dev/null
+++ b/src/presence/GlobalStateAggregator.cpp
@@ -0,0 +1,3 @@
+#include "GlobalStateAggregator.h"
+
+GlobalStateAggregator::GlobalStateAggregator(QObject *parent) : QObject{ parent } { }
diff --git a/src/presence/GlobalStateAggregator.h b/src/presence/GlobalStateAggregator.h
new file mode 100644
index 00000000..db3bea76
--- /dev/null
+++ b/src/presence/GlobalStateAggregator.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include
+#include
+#include "PresenceState.h"
+
+class GlobalStateAggregator : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(PresenceState::State presenceState MEMBER m_presenceState NOTIFY presenceStateChanged
+ FINAL)
+ Q_PROPERTY(QString statusText MEMBER m_statusText NOTIFY statusTextChanged FINAL)
+
+public:
+ static GlobalStateAggregator &instance()
+ {
+ static GlobalStateAggregator *_instance = nullptr;
+ if (!_instance) {
+ _instance = new GlobalStateAggregator;
+ }
+ return *_instance;
+ };
+
+ PresenceState::State presenceState() const { return m_presenceState; }
+ QString statusText() const { return m_statusText; }
+
+private:
+ explicit GlobalStateAggregator(QObject *parent = nullptr);
+
+ PresenceState::State m_presenceState = PresenceState::State::Available;
+ QString m_statusText;
+
+Q_SIGNALS:
+ void presenceStateChanged();
+ void statusTextChanged();
+};
+
+class GlobalStateAggregatorWrapper
+{
+ Q_GADGET
+ QML_FOREIGN(GlobalStateAggregator)
+ QML_NAMED_ELEMENT(GlobalStateAggregator)
+ QML_SINGLETON
+
+public:
+ static GlobalStateAggregator *create(QQmlEngine *, QJSEngine *)
+ {
+ return &GlobalStateAggregator::instance();
+ }
+
+private:
+ GlobalStateAggregatorWrapper() = default;
+};
diff --git a/src/presence/PresenceState.h b/src/presence/PresenceState.h
new file mode 100644
index 00000000..9434b94b
--- /dev/null
+++ b/src/presence/PresenceState.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include
+#include
+
+class PresenceState : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ QML_UNCREATABLE("")
+
+public:
+ explicit PresenceState(QObject *parent = nullptr) : QObject{ parent } { };
+
+ enum class State { Unknown, Offline, Away, Busy, Available, Ringing };
+ Q_ENUM(State)
+};
diff --git a/src/sip/SIPAccount.cpp b/src/sip/SIPAccount.cpp
index c8bde695..2f0b4606 100644
--- a/src/sip/SIPAccount.cpp
+++ b/src/sip/SIPAccount.cpp
@@ -9,6 +9,7 @@
#include "ErrorBus.h"
#include "Credentials.h"
#include "EnumTranslation.h"
+#include "GlobalStateAggregator.h"
#include
@@ -19,10 +20,14 @@ intptr_t SIPAccount::runningMessageIndex = 0;
SIPAccount::SIPAccount(const QString &group, QObject *parent)
: QObject(parent), Account(), m_account(group)
{
+ connect(this, &SIPAccount::isRegisteredChanged, this,
+ &SIPAccount::updatePresenceStateForwarding);
}
void SIPAccount::initialize()
{
+ m_accountConfig.presConfig.publishEnabled = true;
+
bool ok = false;
static QRegularExpression sipURI = QRegularExpression("^(sips?):([^@]+)(?:@(.+))?$");
@@ -1037,3 +1042,74 @@ SIPAccount::~SIPAccount()
qDeleteAll(m_calls);
m_calls.clear();
}
+
+void SIPAccount::updatePresenceStateForwarding()
+{
+ if (isRegistered() && !m_globalStateConnectionContext) {
+ // Establish
+ m_globalStateConnectionContext = new QObject(this);
+ auto &glob = GlobalStateAggregator::instance();
+
+ connect(&glob, &GlobalStateAggregator::presenceStateChanged, m_globalStateConnectionContext,
+ [this]() { forwardPresenceState(); });
+ connect(&glob, &GlobalStateAggregator::statusTextChanged, m_globalStateConnectionContext,
+ [this]() { forwardPresenceState(); });
+ forwardPresenceState();
+
+ } else if (!isRegistered() && m_globalStateConnectionContext) {
+ // Disconnect
+ m_globalStateConnectionContext->deleteLater();
+ m_globalStateConnectionContext = nullptr;
+ }
+}
+
+void SIPAccount::forwardPresenceState()
+{
+ if (!isRegistered()) {
+ return;
+ }
+
+ setOnlineStatus(createPresenceStatusFromGlobal());
+}
+
+pj::PresenceStatus SIPAccount::createPresenceStatusFromGlobal() const
+{
+ auto &glob = GlobalStateAggregator::instance();
+
+ pj::PresenceStatus pjStatus;
+ pjStatus.note = glob.statusText().toStdString();
+
+ switch (glob.presenceState()) {
+
+ case PresenceState::State::Unknown:
+ pjStatus.status = PJSUA_BUDDY_STATUS_UNKNOWN;
+ pjStatus.activity = PJRPID_ACTIVITY_UNKNOWN;
+ break;
+
+ case PresenceState::State::Offline:
+ pjStatus.status = PJSUA_BUDDY_STATUS_OFFLINE;
+ pjStatus.activity = PJRPID_ACTIVITY_UNKNOWN;
+ break;
+ case PresenceState::State::Away:
+ pjStatus.status = PJSUA_BUDDY_STATUS_ONLINE;
+ pjStatus.activity = PJRPID_ACTIVITY_AWAY;
+ break;
+
+ case PresenceState::State::Busy:
+ pjStatus.status = PJSUA_BUDDY_STATUS_ONLINE;
+ pjStatus.activity = PJRPID_ACTIVITY_BUSY;
+ break;
+
+ case PresenceState::State::Available:
+ pjStatus.status = PJSUA_BUDDY_STATUS_ONLINE;
+ pjStatus.activity = PJRPID_ACTIVITY_UNKNOWN;
+ break;
+
+ case PresenceState::State::Ringing:
+ pjStatus.status = PJSUA_BUDDY_STATUS_ONLINE;
+ pjStatus.activity = PJRPID_ACTIVITY_UNKNOWN;
+ break;
+ }
+
+ return pjStatus;
+}
diff --git a/src/sip/SIPAccount.h b/src/sip/SIPAccount.h
index 3f2ad1e9..67f40226 100644
--- a/src/sip/SIPAccount.h
+++ b/src/sip/SIPAccount.h
@@ -84,6 +84,10 @@ class SIPAccount : public QObject, public pj::Account
~SIPAccount();
+private Q_SLOTS:
+ void updatePresenceStateForwarding();
+ void forwardPresenceState();
+
private:
void finalizeInitialization();
@@ -98,6 +102,8 @@ class SIPAccount : public QObject, public pj::Account
void reinitBuddies();
+ pj::PresenceStatus createPresenceStatusFromGlobal() const;
+
QList m_calls;
QList m_buddies;
QList m_transportIds;
@@ -114,6 +120,7 @@ class SIPAccount : public QObject, public pj::Account
bool m_useInstantMessagingWithoutCheck = true;
bool m_rttEnabled = true;
bool m_afterResume = false;
+ QObject *m_globalStateConnectionContext = nullptr;
QString m_account;
QString m_domain;
diff --git a/src/ui/GonnectWindow.qml b/src/ui/GonnectWindow.qml
index 26491dd5..61dc53ec 100644
--- a/src/ui/GonnectWindow.qml
+++ b/src/ui/GonnectWindow.qml
@@ -447,6 +447,9 @@ BaseWindow {
control.ensureVisible()
control.updateTabSelection(control.conferencePageId, GonnectWindow.PageType.Conference)
}
+ function onShowStatusTextEditDialog() {
+ drawerStackView.push("qrc:/qt/qml/base/ui/components/popups/EditStatusText.qml")
+ }
function onFullscreenToggle() {
if (control.visibility === Window.FullScreen) {
control.showNormal()
diff --git a/src/ui/PersonCoinProvider.cpp b/src/ui/PersonCoinProvider.cpp
new file mode 100644
index 00000000..30c5996d
--- /dev/null
+++ b/src/ui/PersonCoinProvider.cpp
@@ -0,0 +1,78 @@
+#include "PersonCoinProvider.h"
+#include "Theme.h"
+#include
+#include
+#include
+
+PersonCoinProvider::PersonCoinProvider() : QQuickImageProvider{ QQuickImageProvider::Image } { }
+
+QImage PersonCoinProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
+{
+ if (id.isEmpty()) {
+ size->setWidth(0);
+ size->setHeight(0);
+ return QImage();
+ }
+
+ // Determine size
+ int w = 100, h = 100;
+
+ if (requestedSize.isValid()) {
+ w = requestedSize.width();
+ h = requestedSize.height();
+
+ if (w != h) {
+ const int minValue = std::min(w, h);
+ w = minValue;
+ h = minValue;
+ }
+ }
+
+ size->setWidth(w);
+ size->setHeight(h);
+
+ const auto path = makePath(id, w);
+ if (QFileInfo::exists(path)) {
+ return QImage(path);
+
+ } else {
+
+ // Draw image
+ QImage image(*size, QImage::Format_ARGB32);
+ QPainter p(&image);
+ Theme &theme = Theme::instance();
+
+ p.setRenderHint(QPainter::Antialiasing);
+ p.setBrush(theme.backgroundInitials());
+ p.setPen(Qt::NoPen);
+ p.drawEllipse(0, 0, w, h);
+
+ // Initials
+ Q_UNUSED(id);
+ QFont font("Noto Sans");
+ font.setPixelSize(0.4 * h);
+ p.setFont(font);
+ p.setPen(theme.foregroundInitials());
+ p.drawText(image.rect(), Qt::AlignCenter, id);
+
+ QFileInfo info(path);
+ QDir dir;
+ dir.mkpath(info.path());
+ image.save(path);
+
+ return image;
+ }
+}
+
+QString PersonCoinProvider::makePath(const QString &id, const int size) const
+{
+ static QString basePath;
+ if (basePath.isEmpty()) {
+ basePath = QString("%1/coin").arg(
+ QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+ QDir dir;
+ dir.mkpath(basePath);
+ }
+
+ return QString("%1/%2/%3.png").arg(basePath).arg(size).arg(id);
+}
diff --git a/src/ui/PersonCoinProvider.h b/src/ui/PersonCoinProvider.h
new file mode 100644
index 00000000..fab216d7
--- /dev/null
+++ b/src/ui/PersonCoinProvider.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include
+
+class PersonCoinProvider : public QQuickImageProvider
+{
+public:
+ PersonCoinProvider();
+
+ virtual QImage requestImage(const QString &id, QSize *size,
+ const QSize &requestedSize) override;
+
+ QString makePath(const QString &id, const int size) const;
+};
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index e37ce5ed..c8fa029c 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -127,6 +127,7 @@ void Theme::updateColorPalette()
m_backgroundInitials = QColor(214, 212, 233);
m_shadowColor = QColor(0, 0, 0, 32);
m_redColor = QColor(224, 27, 36);
+ m_yellowColor = QColor(217, 176, 114);
m_emergencyColor = QColor(0, 136, 85);
m_greenColor = QColor(36, 181, 27);
m_darkGreenColor = QColor(128, 128, 0);
diff --git a/src/ui/Theme.h b/src/ui/Theme.h
index 011409af..3ac22328 100644
--- a/src/ui/Theme.h
+++ b/src/ui/Theme.h
@@ -52,6 +52,7 @@ class Theme : public QObject
Q_PROPERTY(
QColor activeIndicatorColor READ activeIndicatorColor NOTIFY colorPaletteChanged FINAL)
Q_PROPERTY(QColor greenColor READ greenColor NOTIFY colorPaletteChanged FINAL)
+ Q_PROPERTY(QColor yellowColor READ yellowColor NOTIFY colorPaletteChanged FINAL)
Q_PROPERTY(QColor darkGreenColor READ darkGreenColor NOTIFY colorPaletteChanged FINAL)
Q_PROPERTY(QColor paneColor READ paneColor NOTIFY colorPaletteChanged FINAL)
Q_PROPERTY(QColor highContrastColor READ highContrastColor NOTIFY colorPaletteChanged FINAL)
@@ -136,6 +137,7 @@ class Theme : public QObject
QColor backgroundInitials() const { return m_backgroundInitials; }
QColor shadowColor() const { return m_shadowColor; }
QColor redColor() const { return m_redColor; }
+ QColor yellowColor() const { return m_yellowColor; }
QColor emergencyColor() const { return m_emergencyColor; }
QColor activeIndicatorColor() const { return m_activeIndicatorColor; }
QColor greenColor() const { return m_greenColor; }
@@ -245,6 +247,7 @@ private Q_SLOTS:
QColor m_backgroundInitials;
QColor m_shadowColor;
QColor m_redColor;
+ QColor m_yellowColor;
QColor m_emergencyColor;
QColor m_greenColor;
QColor m_darkGreenColor;
diff --git a/src/ui/ViewHelper.h b/src/ui/ViewHelper.h
index 250e0a65..b7e7db53 100644
--- a/src/ui/ViewHelper.h
+++ b/src/ui/ViewHelper.h
@@ -180,6 +180,7 @@ private Q_SLOTS:
void showDialPad();
void showFirstAid();
void showQuitConfirm();
+ void showStatusTextEditDialog();
void showEmergency(QString accountId, int callId, QString displayName);
void hideEmergency();
void showConferenceChat();
diff --git a/src/ui/components/AvatarImage.qml b/src/ui/components/AvatarImage.qml
index eadffbd6..25eb1a99 100644
--- a/src/ui/components/AvatarImage.qml
+++ b/src/ui/components/AvatarImage.qml
@@ -12,54 +12,31 @@ Item {
implicitWidth: control.size
implicitHeight: control.size
- property alias initials: initialsLabel.text
- property alias source: img.source
+ property string initials
+ property url source
property int size: 24
- property alias showBuddyStatus: buddyStatusIndicatorContainer.visible
- property alias buddyStatus: buddyStatusIndicator.status
- property alias isBlocked: buddyStatusIndicator.isBlocked
- property alias isUnregistered: buddyStatusIndicator.isUnregistered
-
- states: [
- State {
- when: control.source.toString() !== "" // toString() is necessary, see QTBUG-63629
- PropertyChanges {
- initialBackground.visible: false
- initialsLabel.visible: false
- opacityMask.visible: true
- }
- }
- ]
+ property alias showPresenceStatus: statusIndicatorContainer.visible
+ property int presenceStatus
+ property bool isBlocked
+ property bool isUnregistered
+ property alias indicatorComponent: statusIndicatorLoader.sourceComponent
- Rectangle {
- id: initialBackground
- anchors.fill: parent
- color: Theme.backgroundInitials
- radius: initialBackground.width / 2
-
- Accessible.ignored: true
- }
-
- Label {
- id: initialsLabel
- anchors.centerIn: parent
- font.pixelSize: 0.4 * control.size
- color: Theme.foregroundInitials
-
- Accessible.role: Accessible.StaticText
- Accessible.name: initialsLabel.text
- Accessible.description: qsTr("Initials of this contact")
- }
+ readonly property bool hasSource: control.source.toString() !== "" // toString() is necessary, see QTBUG-63629
Image {
id: img
- cache: false
+ cache: true
visible: false
anchors.fill: parent
- fillMode: Image.PreserveAspectFit
+ fillMode: Image.PreserveAspectCrop
sourceSize.width: control.size
sourceSize.height: control.size
+ source: control.hasSource
+ ? control.source
+ : (control.initials
+ ? `image://personcoin/${control.initials}`
+ : "")
}
Rectangle {
@@ -74,26 +51,48 @@ Item {
OpacityMask {
id: opacityMask
- visible: false
anchors.fill: parent
source: img
maskSource: mask
}
Rectangle {
- id: buddyStatusIndicatorContainer
+ id: statusIndicatorContainer
color: Theme.backgroundColor
- x: control.width / 2 + Math.sqrt(((control.width / 2) * (control.width / 2)) / 2) - buddyStatusIndicatorContainer.width / 2
- y: buddyStatusIndicatorContainer.x
- width: 10
- height: buddyStatusIndicatorContainer.width
- radius: buddyStatusIndicatorContainer.width / 2
+ x: control.width / 2 + Math.sqrt(((control.width / 2) * (control.width / 2)) / 2) - statusIndicatorContainer.width / 2
+ y: statusIndicatorContainer.x
+ width: 8/24 * control.size
+ height: statusIndicatorContainer.width
+ radius: statusIndicatorContainer.width / 2
visible: false
- BuddyStatusIndicator {
- id: buddyStatusIndicator
+ Loader {
+ id: statusIndicatorLoader
width: parent.width - 2
+ height: statusIndicatorLoader.width
+ active: control.showPresenceStatus
anchors.centerIn: parent
+ sourceComponent: Component {
+ PresenceStatusIndicator {}
+ }
+
+ onItemChanged: () => {
+ const item = statusIndicatorLoader.item
+
+ if (item) {
+ if (item.hasOwnProperty("status")) {
+ item.status = Qt.binding(() => control.presenceStatus)
+ }
+
+ if (item.hasOwnProperty("isBlocked")) {
+ item.isBlocked = Qt.binding(() => control.isBlocked)
+ }
+
+ if (item.hasOwnProperty("isUnregistered")) {
+ item.isUnregistered = Qt.binding(() => control.isUnregistered)
+ }
+ }
+ }
}
Accessible.ignored: true
diff --git a/src/ui/components/CustomWindowHeader.qml b/src/ui/components/CustomWindowHeader.qml
index 603913e0..933ac2ce 100644
--- a/src/ui/components/CustomWindowHeader.qml
+++ b/src/ui/components/CustomWindowHeader.qml
@@ -235,30 +235,10 @@ Rectangle {
size: 28
initials: ViewHelper.initials(ViewHelper.currentUserName)
source: ViewHelper.currentUser?.hasAvatar ? ("file://" + ViewHelper.currentUser.avatarPath) : ""
- showBuddyStatus: avatarImage.hasBuddyState || avatarImage.isUnregistered
- buddyStatus: SIPBuddyState.UNKNOWN
+ showPresenceStatus: true
+ presenceStatus: GlobalStateAggregator.presenceState
isUnregistered: true
- property bool hasBuddyState: ViewHelper.currentUser?.hasBuddyState ?? false
-
- Component.onCompleted: () => {
- avatarImage.updateBuddyStatus()
- }
-
- function updateBuddyStatus() {
- avatarImage.buddyStatus = ViewHelper.currentUser?.hasBuddyState
- ? SIPManager.buddyStatus(ViewHelper.currentUser.subscriptableNumber)
- : SIPBuddyState.UNKNOWN
- }
-
- Connections {
- target: SIPManager
- enabled: ViewHelper.currentUser?.hasBuddyState ?? false
- function onBuddyStateChanged(url : string, status : int) {
- avatarImage.updateBuddyStatus()
- }
- }
-
Connections {
target: SIPAccountManager
function onSipRegisteredChanged(status : bool) {
@@ -269,6 +249,24 @@ Rectangle {
}
}
}
+
+ TapHandler {
+ gesturePolicy: TapHandler.WithinBounds
+ grabPermissions: PointerHandler.ApprovesTakeOverByAnything
+ exclusiveSignals: TapHandler.SingleTap
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onTapped: () => {
+ ownAvatarContextMenuComponent.createObject(avatarImage).popup()
+ }
+ }
+
+ Component {
+ id: ownAvatarContextMenuComponent
+
+ OwnAvatarContextMenu {
+ id: avatarContextMenu
+ }
+ }
}
HeaderIconButton {
diff --git a/src/ui/components/HistoryList.qml b/src/ui/components/HistoryList.qml
index 31e79f54..bb854c63 100644
--- a/src/ui/components/HistoryList.qml
+++ b/src/ui/components/HistoryList.qml
@@ -156,10 +156,11 @@ Item {
initials: ViewHelper.initials(delg.contactName)
source: delg.hasAvatar ? ("file://" + delg.avatarPath) : ""
visible: delg.hasAvatar || delg.name !== ""
- showBuddyStatus: delg.hasBuddyState || delg.isBlocked
- buddyStatus: delg.buddyStatus
+ showPresenceStatus: delg.hasBuddyState || delg.isBlocked
+ presenceStatus: delg.buddyStatus
isBlocked: delg.isBlocked
size: 40
+ indicatorComponent: Component { BuddyStatusIndicator {} }
Layout.preferredWidth: 40
Layout.preferredHeight: 40
diff --git a/src/ui/components/PresenceStatusIndicator.qml b/src/ui/components/PresenceStatusIndicator.qml
new file mode 100644
index 00000000..6a886f90
--- /dev/null
+++ b/src/ui/components/PresenceStatusIndicator.qml
@@ -0,0 +1,124 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls.impl
+import base
+
+/// Colored circle showing a presence status
+Rectangle {
+ id: control
+
+ width: 10
+ height: control.width
+ radius: control.width / 2
+ color: 'transparent'
+ border.width: 0
+ border.color: Theme.borderColor
+
+ /// The status to show (as in PresenceState)
+ property int status: PresenceState.Unknown
+
+ property bool isBlocked: false
+ property bool isUnregistered: false
+
+ Accessible.ignored: true
+
+ SequentialAnimation {
+ id: ringingAnimation
+ running: false
+ loops: Animation.Infinite
+
+ ColorAnimation {
+ target: control
+ property: "color"
+ from: 'transparent'
+ to: Theme.greenColor
+ duration: 1000
+ }
+ ColorAnimation {
+ target: control
+ property: "color"
+ from: Theme.greenColor
+ to: 'transparent'
+ duration: 1000
+ }
+ }
+
+ IconLabel {
+ id: blockedIcon
+ visible: false
+ height: 10
+ width: 10
+ anchors.centerIn: parent
+ icon {
+ width: 10
+ height: 10
+ source: Icons.dialogCancel
+ }
+
+ Accessible.ignored: true
+ }
+
+ states: [
+ State {
+ when: control.isUnregistered
+
+ PropertyChanges {
+ control.color: Theme.borderColor
+ }
+ },
+ State {
+ when: control.isBlocked
+
+ PropertyChanges {
+ control.width: 12
+ control.height: 12
+ control.color: Theme.backgroundColor
+ blockedIcon.visible: true
+ }
+ },
+ State {
+ when: control.status === PresenceState.Unknown
+
+ PropertyChanges {
+ control.border.width: 1
+ }
+ },
+ State {
+ when: control.status === PresenceState.Offline
+
+ PropertyChanges {
+ control.color: Theme.borderColor
+ }
+ },
+ State {
+ when: control.status === PresenceState.Available
+
+ PropertyChanges {
+ control.color: Theme.greenColor
+ }
+ },
+ State {
+ when: control.status === PresenceState.Ringing
+
+ PropertyChanges {
+ ringingAnimation.running: true
+ }
+
+ },
+ State {
+ when: control.status === PresenceState.Away
+
+ PropertyChanges {
+ control.color: Theme.yellowColor
+ }
+ },
+ State {
+ when: control.status === PresenceState.Busy
+
+ PropertyChanges {
+ control.color: Theme.redColor
+ }
+ }
+ ]
+}
diff --git a/src/ui/components/controls/ControlBar.qml b/src/ui/components/controls/ControlBar.qml
index 53b802e6..5a6c36f2 100644
--- a/src/ui/components/controls/ControlBar.qml
+++ b/src/ui/components/controls/ControlBar.qml
@@ -98,30 +98,10 @@ Item {
size: 28
initials: ViewHelper.initials(ViewHelper.currentUserName)
source: ViewHelper.currentUser?.hasAvatar ? ("file://" + ViewHelper.currentUser.avatarPath) : ""
- showBuddyStatus: avatarImage.hasBuddyState || avatarImage.isUnregistered
- buddyStatus: SIPBuddyState.UNKNOWN
+ showPresenceStatus: true
+ presenceStatus: GlobalStateAggregator.presenceState
isUnregistered: true
- property bool hasBuddyState: ViewHelper.currentUser?.hasBuddyState ?? false
-
- Component.onCompleted: () => {
- avatarImage.updateBuddyStatus()
- }
-
- function updateBuddyStatus() {
- avatarImage.buddyStatus = ViewHelper.currentUser?.hasBuddyState
- ? SIPManager.buddyStatus(ViewHelper.currentUser.subscriptableNumber)
- : SIPBuddyState.UNKNOWN
- }
-
- Connections {
- target: SIPManager
- enabled: ViewHelper.currentUser?.hasBuddyState ?? false
- function onBuddyStateChanged(url : string, status : int) {
- avatarImage.updateBuddyStatus()
- }
- }
-
Connections {
target: SIPAccountManager
function onSipRegisteredChanged(status : bool) {
@@ -132,6 +112,26 @@ Item {
}
}
}
+
+ TapHandler {
+ gesturePolicy: TapHandler.WithinBounds
+ grabPermissions: PointerHandler.ApprovesTakeOverByAnything
+ exclusiveSignals: TapHandler.SingleTap
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onTapped: () => {
+ ownAvatarContextMenuComponent.createObject(avatarImage).popup()
+ }
+ }
+
+ Component {
+ id: ownAvatarContextMenuComponent
+
+ OwnAvatarContextMenu {
+ id: avatarContextMenu
+ x: -avatarContextMenu.implicitWidth
+ }
+ }
+
}
}
}
diff --git a/src/ui/components/controls/FavoriteListItemBig.qml b/src/ui/components/controls/FavoriteListItemBig.qml
index a380e1e7..70b570d8 100644
--- a/src/ui/components/controls/FavoriteListItemBig.qml
+++ b/src/ui/components/controls/FavoriteListItemBig.qml
@@ -87,8 +87,9 @@ Item {
size: 40
initials: ViewHelper.initials(delg.name || delg.phoneNumber)
source: delg.hasAvatar ? ("file://" + delg.avatarPath) : ""
- showBuddyStatus: delg.hasBuddyState
- buddyStatus: delg.buddyStatus
+ showPresenceStatus: delg.hasBuddyState
+ presenceStatus: delg.buddyStatus
+ indicatorComponent: Component { BuddyStatusIndicator {} }
anchors {
left: parent.left
leftMargin: 10
diff --git a/src/ui/components/controls/FavoriteListItemSmall.qml b/src/ui/components/controls/FavoriteListItemSmall.qml
index 9b833bb0..2533bfe2 100644
--- a/src/ui/components/controls/FavoriteListItemSmall.qml
+++ b/src/ui/components/controls/FavoriteListItemSmall.qml
@@ -71,8 +71,9 @@ Item {
id: avatarImage
initials: ViewHelper.initials(delg.name || delg.phoneNumber)
source: delg.hasAvatar ? ("file://" + delg.avatarPath) : ""
- showBuddyStatus: delg.hasBuddyState
- buddyStatus: delg.buddyStatus
+ showPresenceStatus: delg.hasBuddyState
+ presenceStatus: delg.buddyStatus
+ indicatorComponent: Component { BuddyStatusIndicator {} }
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
diff --git a/src/ui/components/popups/EditStatusText.qml b/src/ui/components/popups/EditStatusText.qml
new file mode 100644
index 00000000..ee273915
--- /dev/null
+++ b/src/ui/components/popups/EditStatusText.qml
@@ -0,0 +1,90 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls.Material
+import base
+
+Item {
+ id: control
+ implicitWidth: 400
+ implicitHeight: 260
+
+ Keys.onReturnPressed: () => internal.commitChanges()
+ Keys.onEnterPressed: () => internal.commitChanges()
+
+ QtObject {
+ id: internal
+
+ readonly property string trimmedText: contentTextField.text.trim()
+ readonly property bool isModified: internal.trimmedText !== control.text
+
+ function commitChanges() {
+ if (saveButton.enabled && internal.isModified) {
+ GlobalStateAggregator.statusText = internal.trimmedText
+ internal.close()
+ }
+ }
+
+ function close() {
+ if (control.StackView.view) {
+ control.StackView.view.popCurrentItem(StackView.Immediate)
+ }
+ }
+ }
+
+ HeaderIconButton {
+ id: closeButton
+ iconSource: Icons.mobileCloseApp
+ anchors {
+ top: parent.top
+ right: parent.right
+ }
+
+ onClicked: () => internal.close()
+ }
+
+ TextArea {
+ id: contentTextField
+ placeholderText: qsTr("Your status message...")
+ text: GlobalStateAggregator.statusText
+ wrapMode: TextEdit.Wrap
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: closeButton.left
+ bottom: saveButton.top
+ margins: 20
+ }
+
+ Timer {
+ id: initialFocusTimer
+ interval: 20
+ onTriggered: () => {
+ contentTextField.forceActiveFocus()
+ contentTextField.selectAll()
+ }
+ }
+
+ Component.onCompleted: initialFocusTimer.start()
+
+ Keys.onPressed: keyEvent => {
+ if ((keyEvent.modifiers & Qt.ControlModifier) && keyEvent.key === Qt.Key_Return) {
+ saveButton.click()
+ }
+ }
+ }
+
+ Button {
+ id: saveButton
+ text: internal.trimmedText ? qsTr("Set") : qsTr("Remove")
+ highlighted: true
+ enabled: internal.isModified
+ anchors {
+ horizontalCenter: parent.horizontalCenter
+ bottom: parent.bottom
+ bottomMargin: 20
+ }
+
+ onClicked: () => internal.commitChanges()
+ }
+}
diff --git a/src/ui/components/popups/OwnAvatarContextMenu.qml b/src/ui/components/popups/OwnAvatarContextMenu.qml
new file mode 100644
index 00000000..9d62ef39
--- /dev/null
+++ b/src/ui/components/popups/OwnAvatarContextMenu.qml
@@ -0,0 +1,83 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls.Material
+import base
+
+Menu {
+ id: control
+
+ LoggingCategory {
+ id: category
+ name: "gonnect.qml.popup.OwnAvatarContextMenu"
+ defaultLogLevel: LoggingCategory.Info
+ }
+
+ function setPresenceState(presenceState : int) {
+ GlobalStateAggregator.presenceState = presenceState
+ console.log(category, "User setting presence state to", presenceState)
+ }
+
+ function openStatusTextEditPopup() {
+ ViewHelper.showStatusTextEditDialog()
+ }
+
+ MenuItem {
+ id: availableAction
+ text: qsTr("Available")
+ icon {
+ source: Icons.imUserOnline
+ color: "transparent"
+ }
+ onTriggered: () => control.setPresenceState(PresenceState.Available)
+
+ Accessible.role: Accessible.Button
+ Accessible.name: availableAction.text
+ Accessible.focusable: true
+ Accessible.onPressAction: () => control.setPresenceState(PresenceState.Available)
+ }
+
+ MenuItem {
+ id: awayAction
+ text: qsTr("Away")
+ icon {
+ source: Icons.imUserAway
+ color: "transparent"
+ }
+ onTriggered: () => control.setPresenceState(PresenceState.Away)
+
+ Accessible.role: Accessible.Button
+ Accessible.name: awayAction.text
+ Accessible.focusable: true
+ Accessible.onPressAction: () => control.setPresenceState(PresenceState.Away)
+ }
+
+ MenuItem {
+ id: dndAction
+ text: qsTr("Do not disturb")
+ icon {
+ source: Icons.imUserBusy
+ color: "transparent"
+ }
+ onTriggered: () => control.setPresenceState(PresenceState.Busy)
+
+ Accessible.role: Accessible.Button
+ Accessible.name: dndAction.text
+ Accessible.focusable: true
+ Accessible.onPressAction: () => control.setPresenceState(PresenceState.Busy)
+ }
+
+ MenuSeparator { }
+
+ MenuItem {
+ id: setStatusTextAction
+ text: qsTr("Set status text...")
+ icon.source: Icons.editor
+ onTriggered: () => control.openStatusTextEditPopup()
+
+ Accessible.role: Accessible.Button
+ Accessible.name: setStatusTextAction.text
+ Accessible.focusable: true
+ Accessible.onPressAction: () => control.openStatusTextEditPopup()
+ }
+}
diff --git a/src/ui/components/popups/SearchResultPopup.qml b/src/ui/components/popups/SearchResultPopup.qml
index 5c989d41..a55803a3 100644
--- a/src/ui/components/popups/SearchResultPopup.qml
+++ b/src/ui/components/popups/SearchResultPopup.qml
@@ -480,8 +480,9 @@ Popup {
id: avatarImage
initials: ViewHelper.initials(contactDelg.name)
source: contactDelg.hasAvatar ? ("file://" + contactDelg.avatarPath) : ""
- showBuddyStatus: contactDelg.subscriptableNumber !== ""
- buddyStatus: contactDelg.buddyStatus
+ showPresenceStatus: contactDelg.subscriptableNumber !== ""
+ presenceStatus: contactDelg.buddyStatus
+ indicatorComponent: Component { BuddyStatusIndicator {} }
}
}