diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp
new file mode 100644
index 00000000..2b47b7dc
--- /dev/null
+++ b/src/dialogs/AcceptCall.cpp
@@ -0,0 +1,134 @@
+#include <QComboBox>
+#include <QLabel>
+#include <QPushButton>
+#include <QString>
+#include <QVBoxLayout>
+
+#include "ChatPage.h"
+#include "Config.h"
+#include "UserSettingsPage.h"
+#include "Utils.h"
+#include "WebRTCSession.h"
+#include "dialogs/AcceptCall.h"
+#include "ui/Avatar.h"
+
+namespace dialogs {
+
+AcceptCall::AcceptCall(const QString &caller,
+ const QString &displayName,
+ const QString &roomName,
+ const QString &avatarUrl,
+ QSharedPointer<UserSettings> settings,
+ QWidget *parent)
+ : QWidget(parent)
+{
+ std::string errorMessage;
+ if (!WebRTCSession::instance().init(&errorMessage)) {
+ emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+ emit close();
+ return;
+ }
+ audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
+ settings->defaultAudioSource().toStdString());
+ if (audioDevices_.empty()) {
+ emit ChatPage::instance()->showNotification(
+ "Incoming call: No audio sources found.");
+ emit close();
+ return;
+ }
+
+ setAutoFillBackground(true);
+ setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+ setWindowModality(Qt::WindowModal);
+ setAttribute(Qt::WA_DeleteOnClose, true);
+
+ setMinimumWidth(conf::modals::MIN_WIDGET_WIDTH);
+ setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
+
+ auto layout = new QVBoxLayout(this);
+ layout->setSpacing(conf::modals::WIDGET_SPACING);
+ layout->setMargin(conf::modals::WIDGET_MARGIN);
+
+ QFont f;
+ f.setPointSizeF(f.pointSizeF());
+
+ QFont labelFont;
+ labelFont.setWeight(QFont::Medium);
+
+ QLabel *displayNameLabel = nullptr;
+ if (!displayName.isEmpty() && displayName != caller) {
+ displayNameLabel = new QLabel(displayName, this);
+ labelFont.setPointSizeF(f.pointSizeF() * 2);
+ displayNameLabel->setFont(labelFont);
+ displayNameLabel->setAlignment(Qt::AlignCenter);
+ }
+
+ QLabel *callerLabel = new QLabel(caller, this);
+ labelFont.setPointSizeF(f.pointSizeF() * 1.2);
+ callerLabel->setFont(labelFont);
+ callerLabel->setAlignment(Qt::AlignCenter);
+
+ auto avatar = new Avatar(this, QFontMetrics(f).height() * 6);
+ if (!avatarUrl.isEmpty())
+ avatar->setImage(avatarUrl);
+ else
+ avatar->setLetter(utils::firstChar(roomName));
+
+ const int iconSize = 22;
+ QLabel *callTypeIndicator = new QLabel(this);
+ callTypeIndicator->setPixmap(
+ QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(iconSize * 2, iconSize * 2)));
+
+ QLabel *callTypeLabel = new QLabel("Voice Call", this);
+ labelFont.setPointSizeF(f.pointSizeF() * 1.1);
+ callTypeLabel->setFont(labelFont);
+ callTypeLabel->setAlignment(Qt::AlignCenter);
+
+ auto buttonLayout = new QHBoxLayout;
+ buttonLayout->setSpacing(18);
+ acceptBtn_ = new QPushButton(tr("Accept"), this);
+ acceptBtn_->setDefault(true);
+ acceptBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
+ acceptBtn_->setIconSize(QSize(iconSize, iconSize));
+
+ rejectBtn_ = new QPushButton(tr("Reject"), this);
+ rejectBtn_->setIcon(QIcon(":/icons/icons/ui/end-call.png"));
+ rejectBtn_->setIconSize(QSize(iconSize, iconSize));
+ buttonLayout->addWidget(acceptBtn_);
+ buttonLayout->addWidget(rejectBtn_);
+
+ auto deviceLayout = new QHBoxLayout;
+ auto audioLabel = new QLabel(this);
+ audioLabel->setPixmap(
+ QIcon(":/icons/icons/ui/microphone-unmute.png").pixmap(QSize(iconSize, iconSize)));
+
+ auto deviceList = new QComboBox(this);
+ for (const auto &d : audioDevices_)
+ deviceList->addItem(QString::fromStdString(d));
+
+ deviceLayout->addStretch();
+ deviceLayout->addWidget(audioLabel);
+ deviceLayout->addWidget(deviceList);
+
+ if (displayNameLabel)
+ layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
+ layout->addWidget(callerLabel, 0, Qt::AlignCenter);
+ layout->addWidget(avatar, 0, Qt::AlignCenter);
+ layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
+ layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
+ layout->addLayout(buttonLayout);
+ layout->addLayout(deviceLayout);
+
+ connect(acceptBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
+ WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
+ settings->setDefaultAudioSource(
+ QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
+ emit accept();
+ emit close();
+ });
+ connect(rejectBtn_, &QPushButton::clicked, this, [this]() {
+ emit reject();
+ emit close();
+ });
+}
+}
diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h
new file mode 100644
index 00000000..5db8fcfa
--- /dev/null
+++ b/src/dialogs/AcceptCall.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <QSharedPointer>
+#include <QWidget>
+
+class QPushButton;
+class QString;
+class UserSettings;
+
+namespace dialogs {
+
+class AcceptCall : public QWidget
+{
+ Q_OBJECT
+
+public:
+ AcceptCall(const QString &caller,
+ const QString &displayName,
+ const QString &roomName,
+ const QString &avatarUrl,
+ QSharedPointer<UserSettings> settings,
+ QWidget *parent = nullptr);
+
+signals:
+ void accept();
+ void reject();
+
+private:
+ QPushButton *acceptBtn_;
+ QPushButton *rejectBtn_;
+ std::vector<std::string> audioDevices_;
+};
+}
diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp
new file mode 100644
index 00000000..8acdbe88
--- /dev/null
+++ b/src/dialogs/PlaceCall.cpp
@@ -0,0 +1,103 @@
+#include <QComboBox>
+#include <QLabel>
+#include <QPushButton>
+#include <QString>
+#include <QVBoxLayout>
+
+#include "ChatPage.h"
+#include "Config.h"
+#include "UserSettingsPage.h"
+#include "Utils.h"
+#include "WebRTCSession.h"
+#include "dialogs/PlaceCall.h"
+#include "ui/Avatar.h"
+
+namespace dialogs {
+
+PlaceCall::PlaceCall(const QString &callee,
+ const QString &displayName,
+ const QString &roomName,
+ const QString &avatarUrl,
+ QSharedPointer<UserSettings> settings,
+ QWidget *parent)
+ : QWidget(parent)
+{
+ std::string errorMessage;
+ if (!WebRTCSession::instance().init(&errorMessage)) {
+ emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
+ emit close();
+ return;
+ }
+ audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
+ settings->defaultAudioSource().toStdString());
+ if (audioDevices_.empty()) {
+ emit ChatPage::instance()->showNotification("No audio sources found.");
+ emit close();
+ return;
+ }
+
+ setAutoFillBackground(true);
+ setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+ setWindowModality(Qt::WindowModal);
+ setAttribute(Qt::WA_DeleteOnClose, true);
+
+ auto layout = new QVBoxLayout(this);
+ layout->setSpacing(conf::modals::WIDGET_SPACING);
+ layout->setMargin(conf::modals::WIDGET_MARGIN);
+
+ auto buttonLayout = new QHBoxLayout;
+ buttonLayout->setSpacing(15);
+ buttonLayout->setMargin(0);
+
+ QFont f;
+ f.setPointSizeF(f.pointSizeF());
+ auto avatar = new Avatar(this, QFontMetrics(f).height() * 3);
+ if (!avatarUrl.isEmpty())
+ avatar->setImage(avatarUrl);
+ else
+ avatar->setLetter(utils::firstChar(roomName));
+ const int iconSize = 18;
+ voiceBtn_ = new QPushButton(tr("Voice"), this);
+ voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
+ voiceBtn_->setIconSize(QSize(iconSize, iconSize));
+ voiceBtn_->setDefault(true);
+ cancelBtn_ = new QPushButton(tr("Cancel"), this);
+
+ buttonLayout->addWidget(avatar);
+ buttonLayout->addStretch();
+ buttonLayout->addWidget(voiceBtn_);
+ buttonLayout->addWidget(cancelBtn_);
+
+ QString name = displayName.isEmpty() ? callee : displayName;
+ QLabel *label = new QLabel("Place a call to " + name + "?", this);
+
+ auto deviceLayout = new QHBoxLayout;
+ auto audioLabel = new QLabel(this);
+ audioLabel->setPixmap(QIcon(":/icons/icons/ui/microphone-unmute.png")
+ .pixmap(QSize(iconSize * 1.2, iconSize * 1.2)));
+
+ auto deviceList = new QComboBox(this);
+ for (const auto &d : audioDevices_)
+ deviceList->addItem(QString::fromStdString(d));
+
+ deviceLayout->addStretch();
+ deviceLayout->addWidget(audioLabel);
+ deviceLayout->addWidget(deviceList);
+
+ layout->addWidget(label);
+ layout->addLayout(buttonLayout);
+ layout->addLayout(deviceLayout);
+
+ connect(voiceBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
+ WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
+ settings->setDefaultAudioSource(
+ QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
+ emit voice();
+ emit close();
+ });
+ connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
+ emit cancel();
+ emit close();
+ });
+}
+}
diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h
new file mode 100644
index 00000000..e178afc4
--- /dev/null
+++ b/src/dialogs/PlaceCall.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <QSharedPointer>
+#include <QWidget>
+
+class QPushButton;
+class QString;
+class UserSettings;
+
+namespace dialogs {
+
+class PlaceCall : public QWidget
+{
+ Q_OBJECT
+
+public:
+ PlaceCall(const QString &callee,
+ const QString &displayName,
+ const QString &roomName,
+ const QString &avatarUrl,
+ QSharedPointer<UserSettings> settings,
+ QWidget *parent = nullptr);
+
+signals:
+ void voice();
+ void cancel();
+
+private:
+ QPushButton *voiceBtn_;
+ QPushButton *cancelBtn_;
+ std::vector<std::string> audioDevices_;
+};
+}
diff --git a/src/dialogs/UserProfile.cpp b/src/dialogs/UserProfile.cpp
new file mode 100644
index 00000000..086dbb40
--- /dev/null
+++ b/src/dialogs/UserProfile.cpp
@@ -0,0 +1,316 @@
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QMessageBox>
+#include <QShortcut>
+#include <QVBoxLayout>
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Utils.h"
+#include "dialogs/UserProfile.h"
+#include "ui/Avatar.h"
+#include "ui/FlatButton.h"
+
+using namespace dialogs;
+
+Q_DECLARE_METATYPE(std::vector<DeviceInfo>)
+
+constexpr int BUTTON_SIZE = 36;
+constexpr int BUTTON_RADIUS = BUTTON_SIZE / 2;
+constexpr int WIDGET_MARGIN = 20;
+constexpr int TOP_WIDGET_MARGIN = 2 * WIDGET_MARGIN;
+constexpr int WIDGET_SPACING = 15;
+constexpr int TEXT_SPACING = 4;
+constexpr int DEVICE_SPACING = 5;
+
+DeviceItem::DeviceItem(DeviceInfo device, QWidget *parent)
+ : QWidget(parent)
+ , info_(std::move(device))
+{
+ QFont font;
+ font.setBold(true);
+
+ auto deviceIdLabel = new QLabel(info_.device_id, this);
+ deviceIdLabel->setFont(font);
+
+ auto layout = new QVBoxLayout{this};
+ layout->addWidget(deviceIdLabel);
+
+ if (!info_.display_name.isEmpty())
+ layout->addWidget(new QLabel(info_.display_name, this));
+
+ layout->setMargin(0);
+ layout->setSpacing(4);
+}
+
+UserProfile::UserProfile(QWidget *parent)
+ : QWidget(parent)
+{
+ setAutoFillBackground(true);
+ setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
+ setAttribute(Qt::WA_DeleteOnClose, true);
+
+ QIcon banIcon, kickIcon, ignoreIcon, startChatIcon;
+
+ banIcon.addFile(":/icons/icons/ui/do-not-disturb-rounded-sign.png");
+ banBtn_ = new FlatButton(this);
+ banBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ banBtn_->setCornerRadius(BUTTON_RADIUS);
+ banBtn_->setIcon(banIcon);
+ banBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
+ banBtn_->setToolTip(tr("Ban the user from the room"));
+
+ ignoreIcon.addFile(":/icons/icons/ui/volume-off-indicator.png");
+ ignoreBtn_ = new FlatButton(this);
+ ignoreBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ ignoreBtn_->setCornerRadius(BUTTON_RADIUS);
+ ignoreBtn_->setIcon(ignoreIcon);
+ ignoreBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
+ ignoreBtn_->setToolTip(tr("Ignore messages from this user"));
+ ignoreBtn_->setDisabled(true); // Not used yet.
+
+ kickIcon.addFile(":/icons/icons/ui/round-remove-button.png");
+ kickBtn_ = new FlatButton(this);
+ kickBtn_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ kickBtn_->setCornerRadius(BUTTON_RADIUS);
+ kickBtn_->setIcon(kickIcon);
+ kickBtn_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
+ kickBtn_->setToolTip(tr("Kick the user from the room"));
+
+ startChatIcon.addFile(":/icons/icons/ui/black-bubble-speech.png");
+ startChat_ = new FlatButton(this);
+ startChat_->setFixedSize(BUTTON_SIZE, BUTTON_SIZE);
+ startChat_->setCornerRadius(BUTTON_RADIUS);
+ startChat_->setIcon(startChatIcon);
+ startChat_->setIconSize(QSize(BUTTON_RADIUS, BUTTON_RADIUS));
+ startChat_->setToolTip(tr("Start a conversation"));
+
+ connect(startChat_, &QPushButton::clicked, this, [this]() {
+ auto user_id = userIdLabel_->text();
+
+ mtx::requests::CreateRoom req;
+ req.preset = mtx::requests::Preset::PrivateChat;
+ req.visibility = mtx::requests::Visibility::Private;
+
+ if (utils::localUser() != user_id)
+ req.invite = {user_id.toStdString()};
+
+ if (QMessageBox::question(
+ this,
+ tr("Confirm DM"),
+ tr("Do you really want to invite %1 (%2) to a direct chat?")
+ .arg(cache::displayName(roomId_, user_id))
+ .arg(user_id)) != QMessageBox::Yes)
+ return;
+
+ emit ChatPage::instance()->createRoom(req);
+ });
+
+ connect(banBtn_, &QPushButton::clicked, this, [this] {
+ ChatPage::instance()->banUser(userIdLabel_->text(), "");
+ });
+ connect(kickBtn_, &QPushButton::clicked, this, [this] {
+ ChatPage::instance()->kickUser(userIdLabel_->text(), "");
+ });
+
+ // Button line
+ auto btnLayout = new QHBoxLayout;
+ btnLayout->addStretch(1);
+ btnLayout->addWidget(startChat_);
+ btnLayout->addWidget(ignoreBtn_);
+
+ btnLayout->addWidget(kickBtn_);
+ btnLayout->addWidget(banBtn_);
+ btnLayout->addStretch(1);
+ btnLayout->setSpacing(8);
+ btnLayout->setMargin(0);
+
+ avatar_ = new Avatar(this, 128);
+ avatar_->setLetter("X");
+
+ QFont font;
+ font.setPointSizeF(font.pointSizeF() * 2);
+
+ userIdLabel_ = new QLabel(this);
+ displayNameLabel_ = new QLabel(this);
+ displayNameLabel_->setFont(font);
+
+ auto textLayout = new QVBoxLayout;
+ textLayout->addWidget(displayNameLabel_);
+ textLayout->addWidget(userIdLabel_);
+ textLayout->setAlignment(displayNameLabel_, Qt::AlignCenter | Qt::AlignTop);
+ textLayout->setAlignment(userIdLabel_, Qt::AlignCenter | Qt::AlignTop);
+ textLayout->setSpacing(TEXT_SPACING);
+ textLayout->setMargin(0);
+
+ devices_ = new QListWidget{this};
+ devices_->setFrameStyle(QFrame::NoFrame);
+ devices_->setSelectionMode(QAbstractItemView::NoSelection);
+ devices_->setAttribute(Qt::WA_MacShowFocusRect, 0);
+ devices_->setSpacing(DEVICE_SPACING);
+
+ QFont descriptionLabelFont;
+ descriptionLabelFont.setWeight(65);
+
+ devicesLabel_ = new QLabel(tr("Devices").toUpper(), this);
+ devicesLabel_->setFont(descriptionLabelFont);
+ devicesLabel_->hide();
+ devicesLabel_->setFixedSize(devicesLabel_->sizeHint());
+
+ auto okBtn = new QPushButton("OK", this);
+
+ auto closeLayout = new QHBoxLayout();
+ closeLayout->setSpacing(15);
+ closeLayout->addStretch(1);
+ closeLayout->addWidget(okBtn);
+
+ auto vlayout = new QVBoxLayout{this};
+ vlayout->addWidget(avatar_, 0, Qt::AlignCenter | Qt::AlignTop);
+ vlayout->addLayout(textLayout);
+ vlayout->addLayout(btnLayout);
+ vlayout->addWidget(devicesLabel_, 0, Qt::AlignLeft);
+ vlayout->addWidget(devices_, 1);
+ vlayout->addLayout(closeLayout);
+
+ QFont largeFont;
+ largeFont.setPointSizeF(largeFont.pointSizeF() * 1.5);
+
+ setMinimumWidth(
+ std::max(devices_->sizeHint().width() + 4 * WIDGET_MARGIN, conf::window::minModalWidth));
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+
+ vlayout->setSpacing(WIDGET_SPACING);
+ vlayout->setContentsMargins(WIDGET_MARGIN, TOP_WIDGET_MARGIN, WIDGET_MARGIN, WIDGET_MARGIN);
+
+ qRegisterMetaType<std::vector<DeviceInfo>>();
+
+ auto closeShortcut = new QShortcut(QKeySequence(QKeySequence::Cancel), this);
+ connect(closeShortcut, &QShortcut::activated, this, &UserProfile::close);
+ connect(okBtn, &QPushButton::clicked, this, &UserProfile::close);
+}
+
+void
+UserProfile::resetToDefaults()
+{
+ avatar_->setLetter("X");
+ devices_->clear();
+
+ ignoreBtn_->show();
+ devices_->hide();
+ devicesLabel_->hide();
+}
+
+void
+UserProfile::init(const QString &userId, const QString &roomId)
+{
+ resetToDefaults();
+
+ this->roomId_ = roomId;
+
+ auto displayName = cache::displayName(roomId, userId);
+
+ userIdLabel_->setText(userId);
+ displayNameLabel_->setText(displayName);
+ avatar_->setLetter(utils::firstChar(displayName));
+
+ avatar_->setImage(roomId, userId);
+
+ auto localUser = utils::localUser();
+
+ try {
+ bool hasMemberRights =
+ cache::hasEnoughPowerLevel({mtx::events::EventType::RoomMember},
+ roomId.toStdString(),
+ localUser.toStdString());
+ if (!hasMemberRights) {
+ kickBtn_->hide();
+ banBtn_->hide();
+ } else {
+ kickBtn_->show();
+ banBtn_->show();
+ }
+ } catch (const lmdb::error &e) {
+ nhlog::db()->warn("lmdb error: {}", e.what());
+ }
+
+ if (localUser == userId) {
+ // TODO: click on display name & avatar to change.
+ kickBtn_->hide();
+ banBtn_->hide();
+ ignoreBtn_->hide();
+ }
+
+ mtx::requests::QueryKeys req;
+ req.device_keys[userId.toStdString()] = {};
+
+ // A proxy object is used to emit the signal instead of the original object
+ // which might be destroyed by the time the http call finishes.
+ auto proxy = std::make_shared<Proxy>();
+ QObject::connect(proxy.get(), &Proxy::done, this, &UserProfile::updateDeviceList);
+
+ http::client()->query_keys(
+ req,
+ [user_id = userId.toStdString(), proxy = std::move(proxy)](
+ const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
+ if (err) {
+ nhlog::net()->warn("failed to query device keys: {} {}",
+ err->matrix_error.error,
+ static_cast<int>(err->status_code));
+ // TODO: Notify the UI.
+ return;
+ }
+
+ if (res.device_keys.empty() ||
+ (res.device_keys.find(user_id) == res.device_keys.end())) {
+ nhlog::net()->warn("no devices retrieved {}", user_id);
+ return;
+ }
+
+ auto devices = res.device_keys.at(user_id);
+
+ std::vector<DeviceInfo> deviceInfo;
+ for (const auto &d : devices) {
+ auto device = d.second;
+
+ // TODO: Verify signatures and ignore those that don't pass.
+ deviceInfo.emplace_back(DeviceInfo{
+ QString::fromStdString(d.first),
+ QString::fromStdString(device.unsigned_info.device_display_name)});
+ }
+
+ std::sort(deviceInfo.begin(),
+ deviceInfo.end(),
+ [](const DeviceInfo &a, const DeviceInfo &b) {
+ return a.device_id > b.device_id;
+ });
+
+ if (!deviceInfo.empty())
+ emit proxy->done(QString::fromStdString(user_id), deviceInfo);
+ });
+}
+
+void
+UserProfile::updateDeviceList(const QString &user_id, const std::vector<DeviceInfo> &devices)
+{
+ if (user_id != userIdLabel_->text())
+ return;
+
+ for (const auto &dev : devices) {
+ auto deviceItem = new DeviceItem(dev, this);
+ auto item = new QListWidgetItem;
+
+ item->setSizeHint(deviceItem->minimumSizeHint());
+ item->setFlags(Qt::NoItemFlags);
+ item->setTextAlignment(Qt::AlignCenter);
+
+ devices_->insertItem(devices_->count() - 1, item);
+ devices_->setItemWidget(item, deviceItem);
+ }
+
+ devicesLabel_->show();
+ devices_->show();
+ adjustSize();
+}
diff --git a/src/dialogs/UserProfile.h b/src/dialogs/UserProfile.h
new file mode 100644
index 00000000..8129fdcf
--- /dev/null
+++ b/src/dialogs/UserProfile.h
@@ -0,0 +1,70 @@
+#pragma once
+
+#include <QString>
+#include <QWidget>
+
+class Avatar;
+class FlatButton;
+class QLabel;
+class QListWidget;
+class Toggle;
+
+struct DeviceInfo
+{
+ QString device_id;
+ QString display_name;
+};
+
+class Proxy : public QObject
+{
+ Q_OBJECT
+
+signals:
+ void done(const QString &user_id, const std::vector<DeviceInfo> &devices);
+};
+
+namespace dialogs {
+
+class DeviceItem : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit DeviceItem(DeviceInfo device, QWidget *parent);
+
+private:
+ DeviceInfo info_;
+
+ // Toggle *verifyToggle_;
+};
+
+class UserProfile : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit UserProfile(QWidget *parent = nullptr);
+
+ void init(const QString &userId, const QString &roomId);
+
+private slots:
+ void updateDeviceList(const QString &user_id, const std::vector<DeviceInfo> &devices);
+
+private:
+ void resetToDefaults();
+
+ Avatar *avatar_;
+ QString roomId_;
+
+ QLabel *userIdLabel_;
+ QLabel *displayNameLabel_;
+
+ FlatButton *banBtn_;
+ FlatButton *kickBtn_;
+ FlatButton *ignoreBtn_;
+ FlatButton *startChat_;
+
+ QLabel *devicesLabel_;
+ QListWidget *devices_;
+};
+
+} // dialogs
|