From 96f4169be97715e6b6b45663492e3791ba21ae09 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 8 Jun 2020 01:45:24 +0200 Subject: [PATCH] Show presence and set custom status messages --- resources/qml/Avatar.qml | 18 +++++++ resources/qml/TimelineView.qml | 14 +++++- src/Cache.cpp | 72 ++++++++++++++++++++++++++++ src/Cache.h | 6 +++ src/Cache_p.h | 13 +++++ src/ChatPage.cpp | 20 ++++++++ src/ChatPage.h | 6 +++ src/UserInfoWidget.cpp | 24 ++++++++++ src/UserInfoWidget.h | 4 ++ src/timeline/TimelineViewManager.cpp | 12 +++++ src/timeline/TimelineViewManager.h | 3 ++ 11 files changed, 191 insertions(+), 1 deletion(-) diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index 465a8e1c..9ac7b562 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -9,6 +9,7 @@ Rectangle { radius: settings.avatarCircles ? height/2 : 3 property alias url: img.source + property string userid property string displayName Label { @@ -42,6 +43,23 @@ Rectangle { radius: settings.avatarCircles ? height/2 : 3 } } + } + + Rectangle { + anchors.bottom: avatar.bottom + anchors.right: avatar.right + + height: avatar.height / 6 + width: height + radius: settings.avatarCircles ? height / 2 : height / 4 + color: switch (timelineManager.userPresence(userid)) { + case "online": return "#00cc66" + case "unavailable": return "#ff9933" + case "offline": return "#a82353" + default: "transparent" + } + } + color: colors.base } diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 08130033..ea6bf093 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -152,6 +152,8 @@ Page { onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom + property int delegateMaxWidth: (settings.timelineMaxWidth > 100 && (parent.width - settings.timelineMaxWidth) > 32) ? settings.timelineMaxWidth : (parent.width - 32) + delegate: Rectangle { // This would normally be previousSection, but our model's order is inverted. property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1 @@ -159,7 +161,7 @@ Page { id: wrapper property Item section anchors.horizontalCenter: parent.horizontalCenter - width: (settings.timelineMaxWidth > 100 && (parent.width - settings.timelineMaxWidth) > 32) ? settings.timelineMaxWidth : (parent.width - 32) + width: chat.delegateMaxWidth height: section ? section.height + timelinerow.height : timelinerow.height color: "transparent" @@ -236,6 +238,7 @@ Page { height: avatarSize url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") displayName: modelData.userName + userid: modelData.userId MouseArea { anchors.fill: parent @@ -258,6 +261,15 @@ Page { propagateComposedEvents: true } } + + Label { + color: colors.buttonText + text: timelineManager.userStatus(modelData.userId) + textFormat: Text.PlainText + elide: Text.ElideRight + width: chat.delegateMaxWidth - parent.spacing*2 - userName.implicitWidth - avatarSize + font.italic: true + } } } } diff --git a/src/Cache.cpp b/src/Cache.cpp index 009cbabc..d9d1134e 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -952,6 +952,8 @@ Cache::saveState(const mtx::responses::Sync &res) saveInvites(txn, res.rooms.invite); + savePresence(txn, res.presence); + removeLeftRooms(txn, res.rooms.leave); txn.commit(); @@ -1037,6 +1039,21 @@ Cache::saveInvite(lmdb::txn &txn, } } +void +Cache::savePresence( + lmdb::txn &txn, + const std::vector> &presenceUpdates) +{ + for (const auto &update : presenceUpdates) { + auto presenceDb = getPresenceDb(txn); + + lmdb::dbi_put(txn, + presenceDb, + lmdb::val(update.sender), + lmdb::val(json(update.content).dump())); + } +} + std::vector Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) { @@ -2254,6 +2271,50 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id) AvatarUrls.remove(fmt); } +mtx::presence::PresenceState +Cache::presenceState(const std::string &user_id) +{ + lmdb::val presenceVal; + + auto txn = lmdb::txn::begin(env_); + auto db = getPresenceDb(txn); + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), presenceVal); + + mtx::presence::PresenceState state = mtx::presence::offline; + + if (res) { + mtx::events::presence::Presence presence = + json::parse(std::string(presenceVal.data(), presenceVal.size())); + state = presence.presence; + } + + txn.commit(); + + return state; +} + +std::string +Cache::statusMessage(const std::string &user_id) +{ + lmdb::val presenceVal; + + auto txn = lmdb::txn::begin(env_); + auto db = getPresenceDb(txn); + auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), presenceVal); + + std::string status_msg; + + if (res) { + mtx::events::presence::Presence presence = + json::parse(std::string(presenceVal.data(), presenceVal.size())); + status_msg = presence.status_msg; + } + + txn.commit(); + + return status_msg; +} + void to_json(json &j, const RoomInfo &info) { @@ -2425,6 +2486,17 @@ insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &a instance_->insertAvatarUrl(room_id, user_id, avatar_url); } +mtx::presence::PresenceState +presenceState(const std::string &user_id) +{ + return instance_->presenceState(user_id); +} +std::string +statusMessage(const std::string &user_id) +{ + return instance_->statusMessage(user_id); +} + //! Load saved data for the display names & avatars. void populateMembers() diff --git a/src/Cache.h b/src/Cache.h index 12465c9d..b5275623 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -54,6 +54,12 @@ insertDisplayName(const QString &room_id, const QString &user_id, const QString void insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); +// presence +mtx::presence::PresenceState +presenceState(const std::string &user_id); +std::string +statusMessage(const std::string &user_id); + //! Load saved data for the display names & avatars. void populateMembers(); diff --git a/src/Cache_p.h b/src/Cache_p.h index 0d66a608..892b66a5 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -52,6 +52,10 @@ public: static QString displayName(const QString &room_id, const QString &user_id); static QString avatarUrl(const QString &room_id, const QString &user_id); + // presence + mtx::presence::PresenceState presenceState(const std::string &user_id); + std::string statusMessage(const std::string &user_id); + static void removeDisplayName(const QString &room_id, const QString &user_id); static void removeAvatarUrl(const QString &room_id, const QString &user_id); @@ -377,6 +381,10 @@ private: void saveInvites(lmdb::txn &txn, const std::map &rooms); + void savePresence( + lmdb::txn &txn, + const std::vector> &presenceUpdates); + //! Sends signals for the rooms that are removed. void removeLeftRooms(lmdb::txn &txn, const std::map &rooms) @@ -430,6 +438,11 @@ private: return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); } + lmdb::dbi getPresenceDb(lmdb::txn &txn) + { + return lmdb::dbi::open(txn, "presence", MDB_CREATE); + } + //! Retrieves or creates the database that stores the open OLM sessions between our device //! and the given curve25519 key which represents another device. //! diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp index c7f5164a..f2b8253b 100644 --- a/src/ChatPage.cpp +++ b/src/ChatPage.cpp @@ -61,6 +61,7 @@ constexpr size_t MAX_ONETIME_KEYS = 50; Q_DECLARE_METATYPE(std::optional) Q_DECLARE_METATYPE(std::optional) +Q_DECLARE_METATYPE(mtx::presence::PresenceState) ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) : QWidget(parent) @@ -72,6 +73,7 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent) qRegisterMetaType>(); qRegisterMetaType>(); + qRegisterMetaType(); topLayout_ = new QHBoxLayout(this); topLayout_->setSpacing(0); @@ -1221,6 +1223,24 @@ ChatPage::sendTypingNotifications() }); } +QString +ChatPage::status() const +{ + return QString::fromStdString(cache::statusMessage(utils::localUser().toStdString())); +} + +void +ChatPage::setStatus(const QString &status) +{ + http::client()->put_presence_status( + currentPresence(), status.toStdString(), [](mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to set presence status_msg: {}", + err->matrix_error.error); + } + }); +} + void ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) { diff --git a/src/ChatPage.h b/src/ChatPage.h index 83b4e76d..72fc3b81 100644 --- a/src/ChatPage.h +++ b/src/ChatPage.h @@ -89,6 +89,11 @@ public: void initiateLogout(); void focusMessageInput(); + QString status() const; + void setStatus(const QString &status); + + mtx::presence::PresenceState currentPresence() const { return mtx::presence::online; } + public slots: void leaveRoom(const QString &room_id); void createRoom(const mtx::requests::CreateRoom &req); @@ -154,6 +159,7 @@ signals: const QImage &icon); void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); + void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state); void themeChanged(); void decryptSidebarChanged(); diff --git a/src/UserInfoWidget.cpp b/src/UserInfoWidget.cpp index e11aa6aa..38677f25 100644 --- a/src/UserInfoWidget.cpp +++ b/src/UserInfoWidget.cpp @@ -16,7 +16,9 @@ * along with this program. If not, see . */ +#include #include +#include #include #include #include @@ -24,6 +26,7 @@ #include +#include "ChatPage.h" #include "Config.h" #include "MainWindow.h" #include "Splitter.h" @@ -105,6 +108,27 @@ UserInfoWidget::UserInfoWidget(QWidget *parent) connect(logoutButton_, &QPushButton::clicked, this, []() { MainWindow::instance()->openLogoutDialog(); }); + + menu = new QMenu(this); + + auto setStatusAction = menu->addAction(tr("Set custom status message")); + connect(setStatusAction, &QAction::triggered, this, [this]() { + bool ok = false; + QString text = QInputDialog::getText(this, + tr("Custom status message"), + tr("Status:"), + QLineEdit::Normal, + ChatPage::instance()->status(), + &ok); + if (ok && !text.isEmpty()) + ChatPage::instance()->setStatus(text); + }); +} + +void +UserInfoWidget::contextMenuEvent(QContextMenuEvent *event) +{ + menu->popup(event->globalPos()); } void diff --git a/src/UserInfoWidget.h b/src/UserInfoWidget.h index 575ade52..03ab2cf0 100644 --- a/src/UserInfoWidget.h +++ b/src/UserInfoWidget.h @@ -26,6 +26,7 @@ class OverlayModal; class QLabel; class QHBoxLayout; class QVBoxLayout; +class QMenu; class UserInfoWidget : public QWidget { @@ -48,6 +49,7 @@ public: protected: void resizeEvent(QResizeEvent *event) override; void paintEvent(QPaintEvent *event) override; + void contextMenuEvent(QContextMenuEvent *) override; private: Avatar *userAvatar_; @@ -70,4 +72,6 @@ private: int logoutButtonSize_; QColor borderColor_; + + QMenu *menu = nullptr; }; diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 30abe506..ff457af9 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -57,6 +57,18 @@ TimelineViewManager::userColor(QString id, QColor background) return userColors.value(id); } +QString +TimelineViewManager::userPresence(QString id) const +{ + return QString::fromStdString( + mtx::presence::to_string(cache::presenceState(id.toStdString()))); +} +QString +TimelineViewManager::userStatus(QString id) const +{ + return QString::fromStdString(cache::statusMessage(id.toStdString())); +} + TimelineViewManager::TimelineViewManager(QSharedPointer userSettings, QWidget *parent) : imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 45a603af..e677ef20 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -41,6 +41,9 @@ public: Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; Q_INVOKABLE QColor userColor(QString id, QColor background); + Q_INVOKABLE QString userPresence(QString id) const; + Q_INVOKABLE QString userStatus(QString id) const; + signals: void clearRoomMessageCount(QString roomid); void updateRoomsLastMessage(QString roomid, const DescInfo &info);