Show presence and set custom status messages

This commit is contained in:
Nicolas Werner 2020-06-08 01:45:24 +02:00
parent e5a55ab1b9
commit 96f4169be9
11 changed files with 191 additions and 1 deletions

View file

@ -9,6 +9,7 @@ Rectangle {
radius: settings.avatarCircles ? height/2 : 3 radius: settings.avatarCircles ? height/2 : 3
property alias url: img.source property alias url: img.source
property string userid
property string displayName property string displayName
Label { Label {
@ -42,6 +43,23 @@ Rectangle {
radius: settings.avatarCircles ? height/2 : 3 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 color: colors.base
} }

View file

@ -152,6 +152,8 @@ Page {
onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom 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 { delegate: Rectangle {
// This would normally be previousSection, but our model's order is inverted. // 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 property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
@ -159,7 +161,7 @@ Page {
id: wrapper id: wrapper
property Item section property Item section
anchors.horizontalCenter: parent.horizontalCenter 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 height: section ? section.height + timelinerow.height : timelinerow.height
color: "transparent" color: "transparent"
@ -236,6 +238,7 @@ Page {
height: avatarSize height: avatarSize
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
displayName: modelData.userName displayName: modelData.userName
userid: modelData.userId
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@ -258,6 +261,15 @@ Page {
propagateComposedEvents: true 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
}
} }
} }
} }

View file

@ -952,6 +952,8 @@ Cache::saveState(const mtx::responses::Sync &res)
saveInvites(txn, res.rooms.invite); saveInvites(txn, res.rooms.invite);
savePresence(txn, res.presence);
removeLeftRooms(txn, res.rooms.leave); removeLeftRooms(txn, res.rooms.leave);
txn.commit(); txn.commit();
@ -1037,6 +1039,21 @@ Cache::saveInvite(lmdb::txn &txn,
} }
} }
void
Cache::savePresence(
lmdb::txn &txn,
const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &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<std::string> std::vector<std::string>
Cache::roomsWithStateUpdates(const mtx::responses::Sync &res) Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
{ {
@ -2254,6 +2271,50 @@ Cache::removeAvatarUrl(const QString &room_id, const QString &user_id)
AvatarUrls.remove(fmt); 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 void
to_json(json &j, const RoomInfo &info) 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); 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. //! Load saved data for the display names & avatars.
void void
populateMembers() populateMembers()

View file

@ -54,6 +54,12 @@ insertDisplayName(const QString &room_id, const QString &user_id, const QString
void void
insertAvatarUrl(const QString &room_id, const QString &user_id, const QString &avatar_url); 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. //! Load saved data for the display names & avatars.
void void
populateMembers(); populateMembers();

View file

@ -52,6 +52,10 @@ public:
static QString displayName(const QString &room_id, const QString &user_id); static QString displayName(const QString &room_id, const QString &user_id);
static QString avatarUrl(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 removeDisplayName(const QString &room_id, const QString &user_id);
static void removeAvatarUrl(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, void saveInvites(lmdb::txn &txn,
const std::map<std::string, mtx::responses::InvitedRoom> &rooms); const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
void savePresence(
lmdb::txn &txn,
const std::vector<mtx::events::Event<mtx::events::presence::Presence>> &presenceUpdates);
//! Sends signals for the rooms that are removed. //! Sends signals for the rooms that are removed.
void removeLeftRooms(lmdb::txn &txn, void removeLeftRooms(lmdb::txn &txn,
const std::map<std::string, mtx::responses::LeftRoom> &rooms) const std::map<std::string, mtx::responses::LeftRoom> &rooms)
@ -430,6 +438,11 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/mentions").c_str(), MDB_CREATE); 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 //! Retrieves or creates the database that stores the open OLM sessions between our device
//! and the given curve25519 key which represents another device. //! and the given curve25519 key which represents another device.
//! //!

View file

@ -61,6 +61,7 @@ constexpr size_t MAX_ONETIME_KEYS = 50;
Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>) Q_DECLARE_METATYPE(std::optional<mtx::crypto::EncryptedFile>)
Q_DECLARE_METATYPE(std::optional<RelatedInfo>) Q_DECLARE_METATYPE(std::optional<RelatedInfo>)
Q_DECLARE_METATYPE(mtx::presence::PresenceState)
ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent) ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: QWidget(parent) : QWidget(parent)
@ -72,6 +73,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>(); qRegisterMetaType<std::optional<mtx::crypto::EncryptedFile>>();
qRegisterMetaType<std::optional<RelatedInfo>>(); qRegisterMetaType<std::optional<RelatedInfo>>();
qRegisterMetaType<mtx::presence::PresenceState>();
topLayout_ = new QHBoxLayout(this); topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(0); 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 void
ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err) ChatPage::initialSyncHandler(const mtx::responses::Sync &res, mtx::http::RequestErr err)
{ {

View file

@ -89,6 +89,11 @@ public:
void initiateLogout(); void initiateLogout();
void focusMessageInput(); void focusMessageInput();
QString status() const;
void setStatus(const QString &status);
mtx::presence::PresenceState currentPresence() const { return mtx::presence::online; }
public slots: public slots:
void leaveRoom(const QString &room_id); void leaveRoom(const QString &room_id);
void createRoom(const mtx::requests::CreateRoom &req); void createRoom(const mtx::requests::CreateRoom &req);
@ -154,6 +159,7 @@ signals:
const QImage &icon); const QImage &icon);
void updateGroupsInfo(const mtx::responses::JoinedGroups &groups); void updateGroupsInfo(const mtx::responses::JoinedGroups &groups);
void retrievedPresence(const QString &statusMsg, mtx::presence::PresenceState state);
void themeChanged(); void themeChanged();
void decryptSidebarChanged(); void decryptSidebarChanged();

View file

@ -16,7 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <QInputDialog>
#include <QLabel> #include <QLabel>
#include <QMenu>
#include <QPainter> #include <QPainter>
#include <QStyle> #include <QStyle>
#include <QStyleOption> #include <QStyleOption>
@ -24,6 +26,7 @@
#include <iostream> #include <iostream>
#include "ChatPage.h"
#include "Config.h" #include "Config.h"
#include "MainWindow.h" #include "MainWindow.h"
#include "Splitter.h" #include "Splitter.h"
@ -105,6 +108,27 @@ UserInfoWidget::UserInfoWidget(QWidget *parent)
connect(logoutButton_, &QPushButton::clicked, this, []() { connect(logoutButton_, &QPushButton::clicked, this, []() {
MainWindow::instance()->openLogoutDialog(); 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 void

View file

@ -26,6 +26,7 @@ class OverlayModal;
class QLabel; class QLabel;
class QHBoxLayout; class QHBoxLayout;
class QVBoxLayout; class QVBoxLayout;
class QMenu;
class UserInfoWidget : public QWidget class UserInfoWidget : public QWidget
{ {
@ -48,6 +49,7 @@ public:
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void contextMenuEvent(QContextMenuEvent *) override;
private: private:
Avatar *userAvatar_; Avatar *userAvatar_;
@ -70,4 +72,6 @@ private:
int logoutButtonSize_; int logoutButtonSize_;
QColor borderColor_; QColor borderColor_;
QMenu *menu = nullptr;
}; };

View file

@ -57,6 +57,18 @@ TimelineViewManager::userColor(QString id, QColor background)
return userColors.value(id); 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> userSettings, QWidget *parent) TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings, QWidget *parent)
: imgProvider(new MxcImageProvider()) : imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider()) , colorImgProvider(new ColorImageProvider())

View file

@ -41,6 +41,9 @@ public:
Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const; Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const;
signals: signals:
void clearRoomMessageCount(QString roomid); void clearRoomMessageCount(QString roomid);
void updateRoomsLastMessage(QString roomid, const DescInfo &info); void updateRoomsLastMessage(QString roomid, const DescInfo &info);