Merge remote-tracking branch 'origin/master' into cross-signing

This commit is contained in:
Nicolas Werner 2020-09-24 21:36:43 +02:00
commit 4802c34009
43 changed files with 8773 additions and 2200 deletions

View file

@ -34,10 +34,12 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
{
const auto cacheKey = QString("%1_size_%2").arg(avatarUrl).arg(size);
if (avatarUrl.isEmpty())
return;
QPixmap pixmap;
if (avatarUrl.isEmpty()) {
callback(pixmap);
return;
}
if (avatar_cache.find(cacheKey, &pixmap)) {
callback(pixmap);
return;
@ -75,11 +77,10 @@ resolve(const QString &avatarUrl, int size, QObject *receiver, AvatarCallback ca
opts.mxc_url,
mtx::errors::to_string(err->matrix_error.errcode),
err->matrix_error.error);
return;
} else {
cache::saveImage(cacheKey.toStdString(), res);
}
cache::saveImage(cacheKey.toStdString(), res);
emit proxy->avatarDownloaded(QByteArray(res.data(), res.size()));
});
}

View file

@ -96,8 +96,10 @@ namespace {
std::unique_ptr<Cache> instance_ = nullptr;
}
static bool
isHiddenEvent(mtx::events::collections::TimelineEvents e, const std::string &room_id)
bool
Cache::isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
const std::string &room_id)
{
using namespace mtx::events;
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
@ -111,13 +113,27 @@ isHiddenEvent(mtx::events::collections::TimelineEvents e, const std::string &roo
e = result.event.value();
}
static constexpr std::initializer_list<EventType> hiddenEvents = {
mtx::events::account_data::nheko_extensions::HiddenEvents hiddenEvents;
hiddenEvents.hidden_event_types = {
EventType::Reaction, EventType::CallCandidates, EventType::Unsupported};
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, ""))
hiddenEvents = std::move(
std::get<
mtx::events::Event<mtx::events::account_data::nheko_extensions::HiddenEvents>>(
*temp)
.content);
if (auto temp = getAccountData(txn, mtx::events::EventType::NhekoHiddenEvents, room_id))
hiddenEvents = std::move(
std::get<
mtx::events::Event<mtx::events::account_data::nheko_extensions::HiddenEvents>>(
*temp)
.content);
return std::visit(
[](const auto &ev) {
return std::any_of(hiddenEvents.begin(),
hiddenEvents.end(),
[hiddenEvents](const auto &ev) {
return std::any_of(hiddenEvents.hidden_event_types.begin(),
hiddenEvents.hidden_event_types.end(),
[ev](EventType type) { return type == ev.type; });
},
e);
@ -646,6 +662,7 @@ Cache::removeRoom(lmdb::txn &txn, const std::string &roomid)
{
lmdb::dbi_del(txn, roomsDb_, lmdb::val(roomid), nullptr);
lmdb::dbi_drop(txn, getStatesDb(txn, roomid), true);
lmdb::dbi_drop(txn, getAccountDataDb(txn, roomid), true);
lmdb::dbi_drop(txn, getMembersDb(txn, roomid), true);
}
@ -1004,6 +1021,19 @@ Cache::saveState(const mtx::responses::Sync &res)
setNextBatchToken(txn, res.next_batch);
if (!res.account_data.events.empty()) {
auto accountDataDb = getAccountDataDb(txn, "");
for (const auto &ev : res.account_data.events)
std::visit(
[&txn, &accountDataDb](const auto &event) {
lmdb::dbi_put(txn,
accountDataDb,
lmdb::val(to_string(event.type)),
lmdb::val(json(event).dump()));
},
ev);
}
// Save joined rooms
for (const auto &room : res.rooms.join) {
auto statesdb = getStatesDb(txn, room.first);
@ -1023,30 +1053,43 @@ Cache::saveState(const mtx::responses::Sync &res)
updatedInfo.version = getRoomVersion(txn, statesdb).toStdString();
// Process the account_data associated with this room
bool has_new_tags = false;
for (const auto &evt : room.second.account_data.events) {
// for now only fetch tag events
if (std::holds_alternative<Event<account_data::Tags>>(evt)) {
auto tags_evt = std::get<Event<account_data::Tags>>(evt);
has_new_tags = true;
for (const auto &tag : tags_evt.content.tags) {
updatedInfo.tags.push_back(tag.first);
if (!room.second.account_data.events.empty()) {
auto accountDataDb = getAccountDataDb(txn, room.first);
bool has_new_tags = false;
for (const auto &evt : room.second.account_data.events) {
std::visit(
[&txn, &accountDataDb](const auto &event) {
lmdb::dbi_put(txn,
accountDataDb,
lmdb::val(to_string(event.type)),
lmdb::val(json(event).dump()));
},
evt);
// for tag events
if (std::holds_alternative<Event<account_data::Tags>>(evt)) {
auto tags_evt = std::get<Event<account_data::Tags>>(evt);
has_new_tags = true;
for (const auto &tag : tags_evt.content.tags) {
updatedInfo.tags.push_back(tag.first);
}
}
}
}
if (!has_new_tags) {
// retrieve the old tags, they haven't changed
lmdb::val data;
if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room.first), data)) {
try {
RoomInfo tmp =
json::parse(std::string_view(data.data(), data.size()));
updatedInfo.tags = tmp.tags;
} catch (const json::exception &e) {
nhlog::db()->warn(
"failed to parse room info: room_id ({}), {}",
room.first,
std::string(data.data(), data.size()));
if (!has_new_tags) {
// retrieve the old tags, they haven't changed
lmdb::val data;
if (lmdb::dbi_get(txn, roomsDb_, lmdb::val(room.first), data)) {
try {
RoomInfo tmp = json::parse(
std::string_view(data.data(), data.size()));
updatedInfo.tags = tmp.tags;
} catch (const json::exception &e) {
nhlog::db()->warn(
"failed to parse room info: room_id ({}), {}",
room.first,
std::string(data.data(), data.size()));
}
}
}
}
@ -2463,7 +2506,7 @@ Cache::saveTimelineMessages(lmdb::txn &txn,
lmdb::dbi_put(txn, evToOrderDb, event_id, lmdb::val(&index, sizeof(index)));
// TODO(Nico): Allow blacklisting more event types in UI
if (!isHiddenEvent(e, room_id)) {
if (!isHiddenEvent(txn, e, room_id)) {
++msgIndex;
lmdb::cursor_put(msgCursor.handle(),
lmdb::val(&msgIndex, sizeof(msgIndex)),
@ -2546,7 +2589,7 @@ Cache::saveOldMessages(const std::string &room_id, const mtx::responses::Message
lmdb::dbi_put(txn, evToOrderDb, event_id, lmdb::val(&index, sizeof(index)));
// TODO(Nico): Allow blacklisting more event types in UI
if (!isHiddenEvent(e, room_id)) {
if (!isHiddenEvent(txn, e, room_id)) {
--msgIndex;
lmdb::dbi_put(
txn, order2msgDb, lmdb::val(&msgIndex, sizeof(msgIndex)), event_id);
@ -2864,6 +2907,24 @@ Cache::deleteOldData() noexcept
}
}
std::optional<mtx::events::collections::RoomAccountDataEvents>
Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
{
try {
auto db = getAccountDataDb(txn, room_id);
lmdb::val data;
if (lmdb::dbi_get(txn, db, lmdb::val(to_string(type)), data)) {
mtx::responses::utils::RoomAccountDataEvents events;
mtx::responses::utils::parse_room_account_data_events(
std::string_view(data.data(), data.size()), events);
return events.front();
}
} catch (...) {
}
return std::nullopt;
}
bool
Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
const std::string &room_id,

View file

@ -301,6 +301,14 @@ private:
const std::string &room_id,
const mtx::responses::Timeline &res);
//! retrieve a specific event from account data
//! pass empty room_id for global account data
std::optional<mtx::events::collections::RoomAccountDataEvents>
getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id);
bool isHiddenEvent(lmdb::txn &txn,
mtx::events::collections::TimelineEvents e,
const std::string &room_id);
//! Remove a room from the cache.
// void removeLeftRoom(lmdb::txn &txn, const std::string &room_id);
template<class T>
@ -510,6 +518,12 @@ private:
return lmdb::dbi::open(txn, std::string(room_id + "/state").c_str(), MDB_CREATE);
}
lmdb::dbi getAccountDataDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(
txn, std::string(room_id + "/account_data").c_str(), MDB_CREATE);
}
lmdb::dbi getMembersDb(lmdb::txn &txn, const std::string &room_id)
{
return lmdb::dbi::open(txn, std::string(room_id + "/members").c_str(), MDB_CREATE);

View file

@ -38,7 +38,6 @@
#include "SideBarActions.h"
#include "Splitter.h"
#include "TextInputWidget.h"
#include "TopRoomBar.h"
#include "UserInfoWidget.h"
#include "UserSettingsPage.h"
#include "Utils.h"
@ -127,10 +126,8 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
contentLayout_->setSpacing(0);
contentLayout_->setMargin(0);
top_bar_ = new TopRoomBar(this);
view_manager_ = new TimelineViewManager(userSettings_, &callManager_, this);
contentLayout_->addWidget(top_bar_);
contentLayout_->addWidget(view_manager_->getWidget());
activeCallBar_ = new ActiveCallBar(this);
@ -182,30 +179,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
room_list_->previousRoom();
});
connect(top_bar_, &TopRoomBar::mentionsClicked, this, [this](const QPoint &mentionsPos) {
if (user_mentions_popup_->isVisible()) {
user_mentions_popup_->hide();
} else {
showNotificationsDialog(mentionsPos);
http::client()->notifications(
1000,
"",
"highlight",
[this, mentionsPos](const mtx::responses::Notifications &res,
mtx::http::RequestErr err) {
if (err) {
nhlog::net()->warn(
"failed to retrieve notifications: {} ({})",
err->matrix_error.error,
static_cast<int>(err->status_code));
return;
}
emit highlightedNotifsRetrieved(std::move(res), mentionsPos);
});
}
});
connectivityTimer_.setInterval(CHECK_CONNECTIVITY_INTERVAL);
connect(&connectivityTimer_, &QTimer::timeout, this, [=]() {
if (http::client()->access_token().empty()) {
@ -227,8 +200,9 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
connect(this, &ChatPage::loggedOut, this, &ChatPage::logout);
connect(top_bar_, &TopRoomBar::showRoomList, splitter, &Splitter::showFullRoomList);
connect(top_bar_, &TopRoomBar::inviteUsers, this, [this](QStringList users) {
connect(
view_manager_, &TimelineViewManager::showRoomList, splitter, &Splitter::showFullRoomList);
connect(view_manager_, &TimelineViewManager::inviteUsers, this, [this](QStringList users) {
const auto room_id = current_room_.toStdString();
for (int ii = 0; ii < users.size(); ++ii) {
@ -252,8 +226,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
});
connect(room_list_, &RoomList::roomChanged, this, [this](QString room_id) {
this->current_room_ = room_id;
});
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::stopTyping);
connect(room_list_, &RoomList::roomChanged, this, &ChatPage::changeTopRoomInfo);
connect(room_list_, &RoomList::roomChanged, splitter, &Splitter::showChatView);
connect(room_list_, &RoomList::roomChanged, text_input_, &TextInputWidget::focusLineEdit);
connect(
@ -488,8 +464,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
}
});
connect(room_list_, &RoomList::roomAvatarChanged, this, &ChatPage::updateTopBarAvatar);
connect(
this, &ChatPage::updateGroupsInfo, communitiesList_, &CommunitiesList::setCommunities);
@ -589,11 +563,6 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
});
connect(this, &ChatPage::syncRoomlist, room_list_, &RoomList::sync);
connect(this, &ChatPage::syncTags, communitiesList_, &CommunitiesList::syncTags);
connect(
this, &ChatPage::syncTopBar, this, [this](const std::map<QString, RoomInfo> &updates) {
if (updates.find(currentRoom()) != updates.end())
changeTopRoomInfo(currentRoom());
});
// Callbacks to update the user info (top left corner of the page).
connect(this, &ChatPage::setUserAvatar, user_info_widget_, &UserInfoWidget::setAvatar);
@ -658,7 +627,6 @@ void
ChatPage::resetUI()
{
room_list_->clear();
top_bar_->reset();
user_info_widget_->reset();
view_manager_->clearAll();
@ -787,46 +755,6 @@ ChatPage::bootstrap(QString userid, QString homeserver, QString token)
tryInitialSync();
}
void
ChatPage::updateTopBarAvatar(const QString &roomid, const QString &img)
{
if (current_room_ != roomid)
return;
top_bar_->updateRoomAvatar(img);
}
void
ChatPage::changeTopRoomInfo(const QString &room_id)
{
if (room_id.isEmpty()) {
nhlog::ui()->warn("cannot switch to empty room_id");
return;
}
try {
auto room_info = cache::getRoomInfo({room_id.toStdString()});
if (room_info.find(room_id) == room_info.end())
return;
const auto name = QString::fromStdString(room_info[room_id].name);
const auto avatar_url = QString::fromStdString(room_info[room_id].avatar_url);
top_bar_->updateRoomName(name);
top_bar_->updateRoomTopic(QString::fromStdString(room_info[room_id].topic));
top_bar_->updateRoomAvatarFromName(name);
if (!avatar_url.isEmpty())
top_bar_->updateRoomAvatar(avatar_url);
} catch (const lmdb::error &e) {
nhlog::ui()->error("failed to change top bar room info: {}", e.what());
}
current_room_ = room_id;
}
void
ChatPage::showUnreadMessageNotification(int count)
{
@ -968,14 +896,22 @@ ChatPage::sendNotifications(const mtx::responses::Notifications &res)
}
if (userSettings_->hasDesktopNotifications()) {
notificationsManager.postNotification(
room_id,
QString::fromStdString(event_id),
QString::fromStdString(
cache::singleRoomInfo(item.room_id).name),
cache::displayName(room_id, user_id),
utils::event_body(item.event),
cache::getRoomAvatar(room_id));
auto info = cache::singleRoomInfo(item.room_id);
AvatarProvider::resolve(
QString::fromStdString(info.avatar_url),
96,
this,
[this, room_id, event_id, item, user_id, info](
QPixmap image) {
notificationsManager.postNotification(
room_id,
QString::fromStdString(event_id),
QString::fromStdString(info.name),
cache::displayName(room_id, user_id),
utils::event_body(item.event),
image.toImage());
});
}
}
} catch (const lmdb::error &e) {
@ -1071,7 +1007,6 @@ ChatPage::handleSyncResponse(mtx::responses::Sync res)
auto updates = cache::roomUpdates(res);
emit syncTopBar(updates);
emit syncRoomlist(updates);
emit syncUI(res.rooms);
@ -1482,9 +1417,12 @@ ChatPage::getProfileInfo()
void
ChatPage::hideSideBars()
{
communitiesList_->hide();
sideBar_->hide();
top_bar_->enableBackButton();
// Don't hide side bar, if we are currently only showing the side bar!
if (view_manager_->getWidget()->isVisible()) {
communitiesList_->hide();
sideBar_->hide();
}
view_manager_->enableBackButton();
}
void
@ -1494,23 +1432,19 @@ ChatPage::showSideBars()
communitiesList_->show();
sideBar_->show();
top_bar_->disableBackButton();
view_manager_->disableBackButton();
content_->show();
}
uint64_t
ChatPage::timelineWidth()
{
int sidebarWidth = sideBar_->size().width();
sidebarWidth += communitiesList_->size().width();
int sidebarWidth = sideBar_->minimumSize().width();
sidebarWidth += communitiesList_->minimumSize().width();
nhlog::ui()->info("timelineWidth: {}", size().width() - sidebarWidth);
return size().width() - sidebarWidth;
}
bool
ChatPage::isSideBarExpanded()
{
const auto sz = splitter::calculateSidebarSizes(QFont{});
return sideBar_->size().width() > sz.normal;
}
void
ChatPage::initiateLogout()

View file

@ -50,7 +50,6 @@ class SideBarActions;
class Splitter;
class TextInputWidget;
class TimelineViewManager;
class TopRoomBar;
class UserInfoWidget;
class UserSettings;
class NotificationsManager;
@ -85,7 +84,6 @@ public:
//! Calculate the width of the message timeline.
uint64_t timelineWidth();
bool isSideBarExpanded();
//! Hide the room & group list (if it was visible).
void hideSideBars();
//! Show the room/group list (if it was visible).
@ -156,7 +154,6 @@ signals:
void syncUI(const mtx::responses::Rooms &rooms);
void syncRoomlist(const std::map<QString, RoomInfo> &updates);
void syncTags(const std::map<QString, RoomInfo> &updates);
void syncTopBar(const std::map<QString, RoomInfo> &updates);
void dropToLoginPageCb(const QString &msg);
void notifyMessage(const QString &roomid,
@ -191,8 +188,6 @@ signals:
private slots:
void showUnreadMessageNotification(int count);
void updateTopBarAvatar(const QString &roomid, const QString &img);
void changeTopRoomInfo(const QString &room_id);
void logout();
void removeRoom(const QString &room_id);
void dropToLoginPage(const QString &msg);
@ -263,7 +258,6 @@ private:
TimelineViewManager *view_manager_;
SideBarActions *sidebarActions_;
TopRoomBar *top_bar_;
TextInputWidget *text_input_;
ActiveCallBar *activeCallBar_;

View file

@ -200,7 +200,8 @@ MainWindow::adjustSideBars()
const uint64_t timelineWidth = chat_page_->timelineWidth();
const uint64_t minAvailableWidth = sz.collapsePoint + sz.groups;
if (timelineWidth < minAvailableWidth && !chat_page_->isSideBarExpanded()) {
nhlog::ui()->info("timelineWidth: {}, min {}", timelineWidth, minAvailableWidth);
if (timelineWidth < minAvailableWidth) {
chat_page_->hideSideBars();
} else {
chat_page_->showSideBars();
@ -330,9 +331,7 @@ MainWindow::hasActiveUser()
void
MainWindow::openRoomSettings(const QString &room_id)
{
const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : "";
auto dialog = new dialogs::RoomSettings(roomToSearch, this);
auto dialog = new dialogs::RoomSettings(room_id, this);
showDialog(dialog);
}
@ -340,8 +339,7 @@ MainWindow::openRoomSettings(const QString &room_id)
void
MainWindow::openMemberListDialog(const QString &room_id)
{
const auto roomToSearch = room_id.isEmpty() ? chat_page_->currentRoom() : "";
auto dialog = new dialogs::MemberList(roomToSearch, this);
auto dialog = new dialogs::MemberList(room_id, this);
showDialog(dialog);
}
@ -349,11 +347,9 @@ MainWindow::openMemberListDialog(const QString &room_id)
void
MainWindow::openLeaveRoomDialog(const QString &room_id)
{
auto roomToLeave = room_id.isEmpty() ? chat_page_->currentRoom() : room_id;
auto dialog = new dialogs::LeaveRoom(this);
connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, roomToLeave]() {
chat_page_->leaveRoom(roomToLeave);
connect(dialog, &dialogs::LeaveRoom::leaving, this, [this, room_id]() {
chat_page_->leaveRoom(room_id);
});
showDialog(dialog);

View file

@ -67,14 +67,14 @@ public:
static MainWindow *instance() { return instance_; };
void saveCurrentWindowSize();
void openLeaveRoomDialog(const QString &room_id = "");
void openLeaveRoomDialog(const QString &room_id);
void openInviteUsersDialog(std::function<void(const QStringList &invitees)> callback);
void openCreateRoomDialog(
std::function<void(const mtx::requests::CreateRoom &request)> callback);
void openJoinRoomDialog(std::function<void(const QString &room_id)> callback);
void openLogoutDialog();
void openRoomSettings(const QString &room_id = "");
void openMemberListDialog(const QString &room_id = "");
void openRoomSettings(const QString &room_id);
void openMemberListDialog(const QString &room_id);
void openReadReceiptsDialog(const QString &event_id);
void hideOverlay();

View file

@ -17,13 +17,16 @@ MxcImageResponse::run()
auto data = cache::image(fileName);
if (!data.isNull()) {
m_image = utils::readImage(&data);
m_image = m_image.scaled(
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_image.setText("mxc url", "mxc://" + m_id);
if (!m_image.isNull()) {
emit finished();
return;
m_image = m_image.scaled(
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
m_image.setText("mxc url", "mxc://" + m_id);
if (!m_image.isNull()) {
emit finished();
return;
}
}
}
@ -34,7 +37,7 @@ MxcImageResponse::run()
opts.method = "crop";
http::client()->get_thumbnail(
opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) {
if (err) {
if (err || res.empty()) {
nhlog::net()->error("Failed to download image {}",
m_id.toStdString());
m_error = "Failed download";
@ -46,6 +49,10 @@ MxcImageResponse::run()
auto data = QByteArray(res.data(), res.size());
cache::saveImage(fileName, data);
m_image = utils::readImage(&data);
if (!m_image.isNull()) {
m_image = m_image.scaled(
m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
m_image.setText("mxc url", "mxc://" + m_id);
emit finished();

View file

@ -246,7 +246,8 @@ FilteredTextEdit::keyPressEvent(QKeyEvent *event)
}
case Qt::Key_Colon: {
QTextEdit::keyPressEvent(event);
trigger_pos_ = textCursor().position() - 1;
trigger_pos_ = textCursor().position() - 1;
emoji_completion_model_->setFilterRegExp("");
emoji_popup_open_ = true;
break;
}

View file

@ -1,229 +0,0 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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/>.
*/
#include <QAction>
#include <QIcon>
#include <QLabel>
#include <QPaintEvent>
#include <QPainter>
#include <QPen>
#include <QPoint>
#include <QStyle>
#include <QStyleOption>
#include <QVBoxLayout>
#include "Config.h"
#include "MainWindow.h"
#include "TopRoomBar.h"
#include "Utils.h"
#include "ui/Avatar.h"
#include "ui/FlatButton.h"
#include "ui/Menu.h"
#include "ui/OverlayModal.h"
#include "ui/TextLabel.h"
TopRoomBar::TopRoomBar(QWidget *parent)
: QWidget(parent)
, buttonSize_{32}
{
QFont f;
f.setPointSizeF(f.pointSizeF());
const int fontHeight = QFontMetrics(f).height();
const int widgetMargin = fontHeight / 3;
const int contentHeight = fontHeight * 3;
setFixedHeight(contentHeight + widgetMargin);
topLayout_ = new QHBoxLayout(this);
topLayout_->setSpacing(widgetMargin);
topLayout_->setContentsMargins(
2 * widgetMargin, widgetMargin, 2 * widgetMargin, widgetMargin);
avatar_ = new Avatar(this, fontHeight * 2);
avatar_->setLetter("");
textLayout_ = new QVBoxLayout();
textLayout_->setSpacing(0);
textLayout_->setMargin(0);
QFont roomFont;
roomFont.setPointSizeF(roomFont.pointSizeF() * 1.1);
roomFont.setWeight(QFont::Medium);
nameLabel_ = new QLabel(this);
nameLabel_->setFont(roomFont);
nameLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
QFont descriptionFont;
topicLabel_ = new TextLabel(this);
topicLabel_->setLineWrapMode(QTextEdit::NoWrap);
topicLabel_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
topicLabel_->setFont(descriptionFont);
topicLabel_->setTextInteractionFlags(Qt::TextBrowserInteraction);
topicLabel_->setOpenExternalLinks(true);
topicLabel_->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
textLayout_->addWidget(nameLabel_);
textLayout_->addWidget(topicLabel_);
settingsBtn_ = new FlatButton(this);
settingsBtn_->setToolTip(tr("Room options"));
settingsBtn_->setFixedSize(buttonSize_, buttonSize_);
settingsBtn_->setCornerRadius(buttonSize_ / 2);
mentionsBtn_ = new FlatButton(this);
mentionsBtn_->setToolTip(tr("Mentions"));
mentionsBtn_->setFixedSize(buttonSize_, buttonSize_);
mentionsBtn_->setCornerRadius(buttonSize_ / 2);
QIcon settings_icon;
settings_icon.addFile(":/icons/icons/ui/vertical-ellipsis.png");
settingsBtn_->setIcon(settings_icon);
settingsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
QIcon mentions_icon;
mentions_icon.addFile(":/icons/icons/ui/at-solid.svg");
mentionsBtn_->setIcon(mentions_icon);
mentionsBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
backBtn_ = new FlatButton(this);
backBtn_->setFixedSize(buttonSize_, buttonSize_);
backBtn_->setCornerRadius(buttonSize_ / 2);
QIcon backIcon;
backIcon.addFile(":/icons/icons/ui/angle-pointing-to-left.png");
backBtn_->setIcon(backIcon);
backBtn_->setIconSize(QSize(buttonSize_ / 2, buttonSize_ / 2));
backBtn_->hide();
connect(backBtn_, &QPushButton::clicked, this, &TopRoomBar::showRoomList);
topLayout_->addWidget(avatar_);
topLayout_->addWidget(backBtn_);
topLayout_->addLayout(textLayout_, 1);
topLayout_->addWidget(mentionsBtn_, 0, Qt::AlignRight);
topLayout_->addWidget(settingsBtn_, 0, Qt::AlignRight);
menu_ = new Menu(this);
inviteUsers_ = new QAction(tr("Invite users"), this);
connect(inviteUsers_, &QAction::triggered, this, [this]() {
MainWindow::instance()->openInviteUsersDialog(
[this](const QStringList &invitees) { emit inviteUsers(invitees); });
});
roomMembers_ = new QAction(tr("Members"), this);
connect(roomMembers_, &QAction::triggered, this, []() {
MainWindow::instance()->openMemberListDialog();
});
leaveRoom_ = new QAction(tr("Leave room"), this);
connect(leaveRoom_, &QAction::triggered, this, []() {
MainWindow::instance()->openLeaveRoomDialog();
});
roomSettings_ = new QAction(tr("Settings"), this);
connect(roomSettings_, &QAction::triggered, this, []() {
MainWindow::instance()->openRoomSettings();
});
menu_->addAction(inviteUsers_);
menu_->addAction(roomMembers_);
menu_->addAction(leaveRoom_);
menu_->addAction(roomSettings_);
connect(settingsBtn_, &QPushButton::clicked, this, [this]() {
auto pos = mapToGlobal(settingsBtn_->pos());
menu_->popup(
QPoint(pos.x() + buttonSize_ - menu_->sizeHint().width(), pos.y() + buttonSize_));
});
connect(mentionsBtn_, &QPushButton::clicked, this, [this]() {
auto pos = mapToGlobal(mentionsBtn_->pos());
emit mentionsClicked(pos);
});
}
void
TopRoomBar::enableBackButton()
{
avatar_->hide();
backBtn_->show();
}
void
TopRoomBar::disableBackButton()
{
avatar_->show();
backBtn_->hide();
}
void
TopRoomBar::updateRoomAvatarFromName(const QString &name)
{
avatar_->setLetter(utils::firstChar(name));
update();
}
void
TopRoomBar::reset()
{
nameLabel_->setText("");
topicLabel_->setText("");
avatar_->setLetter("");
}
void
TopRoomBar::updateRoomAvatar(const QString &avatar_image)
{
avatar_->setImage(avatar_image);
update();
}
void
TopRoomBar::updateRoomName(const QString &name)
{
nameLabel_->setText(name);
update();
}
void
TopRoomBar::updateRoomTopic(QString topic)
{
topic.replace(conf::strings::url_regex, conf::strings::url_html);
topicLabel_->clearLinks();
topicLabel_->setHtml(topic);
update();
}
void
TopRoomBar::mousePressEvent(QMouseEvent *)
{
if (roomSettings_ != nullptr)
roomSettings_->trigger();
}
void
TopRoomBar::paintEvent(QPaintEvent *)
{
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

View file

@ -1,90 +0,0 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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/>.
*/
#pragma once
#include <QColor>
#include <QStringList>
#include <QWidget>
class Avatar;
class FlatButton;
class Menu;
class TextLabel;
class OverlayModal;
class QLabel;
class QHBoxLayout;
class QVBoxLayout;
class TopRoomBar : public QWidget
{
Q_OBJECT
Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor)
public:
TopRoomBar(QWidget *parent = nullptr);
void updateRoomAvatar(const QString &avatar_image);
void updateRoomName(const QString &name);
void updateRoomTopic(QString topic);
void updateRoomAvatarFromName(const QString &name);
void reset();
QColor borderColor() const { return borderColor_; }
void setBorderColor(QColor &color) { borderColor_ = color; }
public slots:
//! Add a "back-arrow" button that can switch to roomlist only view.
void enableBackButton();
//! Replace the "back-arrow" button with the avatar of the room.
void disableBackButton();
signals:
void inviteUsers(QStringList users);
void showRoomList();
void mentionsClicked(const QPoint &pos);
protected:
void mousePressEvent(QMouseEvent *) override;
void paintEvent(QPaintEvent *) override;
private:
QHBoxLayout *topLayout_ = nullptr;
QVBoxLayout *textLayout_ = nullptr;
QLabel *nameLabel_ = nullptr;
TextLabel *topicLabel_ = nullptr;
Menu *menu_;
QAction *leaveRoom_ = nullptr;
QAction *roomMembers_ = nullptr;
QAction *roomSettings_ = nullptr;
QAction *inviteUsers_ = nullptr;
FlatButton *settingsBtn_;
FlatButton *mentionsBtn_;
FlatButton *backBtn_;
Avatar *avatar_;
int buttonSize_;
QColor borderColor_;
};

View file

@ -513,9 +513,6 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
callsLabel->setFont(font);
useStunServer_ = new Toggle{this};
defaultAudioSourceValue_ = new QLabel(this);
defaultAudioSourceValue_->setFont(font);
auto encryptionLabel_ = new QLabel{tr("ENCRYPTION"), this};
encryptionLabel_->setFixedHeight(encryptionLabel_->minimumHeight() + LayoutTopMargin);
encryptionLabel_->setAlignment(Qt::AlignBottom);
@ -652,7 +649,6 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
boxWrap(tr("Allow fallback call assist server"),
useStunServer_,
tr("Will use turn.matrix.org as assist when your home server does not offer one."));
boxWrap(tr("Default audio source device"), defaultAudioSourceValue_);
formLayout_->addRow(encryptionLabel_);
formLayout_->addRow(new HorizontalLine{this});
@ -813,7 +809,6 @@ UserSettingsPage::showEvent(QShowEvent *)
deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth());
useStunServer_->setState(!settings_->useStunServer());
defaultAudioSourceValue_->setText(settings_->defaultAudioSource());
deviceFingerprintValue_->setText(
utils::humanReadableFingerprint(olm::client()->identity_keys().ed25519));

View file

@ -250,7 +250,6 @@ private:
Toggle *decryptSidebar_;
QLabel *deviceFingerprintValue_;
QLabel *deviceIdValue_;
QLabel *defaultAudioSourceValue_;
QComboBox *themeCombo_;
QComboBox *scaleFactorCombo_;

View file

@ -21,6 +21,7 @@ WebRTCSession::WebRTCSession()
{
qRegisterMetaType<WebRTCSession::State>();
connect(this, &WebRTCSession::stateChanged, this, &WebRTCSession::setState);
init();
}
bool
@ -78,7 +79,11 @@ WebRTCSession::init(std::string *errorMessage)
gst_object_unref(plugin);
}
if (!initialised_) {
if (initialised_) {
#if GST_CHECK_VERSION(1, 18, 0)
startDeviceMonitor();
#endif
} else {
nhlog::ui()->error(strError);
if (errorMessage)
*errorMessage = strError;
@ -95,12 +100,65 @@ namespace {
bool isoffering_;
std::string localsdp_;
std::vector<mtx::events::msg::CallCandidates::Candidate> localcandidates_;
std::vector<std::pair<std::string, GstDevice *>> audioSources_;
void
addDevice(GstDevice *device)
{
if (device) {
gchar *name = gst_device_get_display_name(device);
nhlog::ui()->debug("WebRTC: device added: {}", name);
audioSources_.push_back({name, device});
g_free(name);
}
}
#if GST_CHECK_VERSION(1, 18, 0)
void
removeDevice(GstDevice *device, bool changed)
{
if (device) {
if (auto it = std::find_if(audioSources_.begin(),
audioSources_.end(),
[device](const auto &s) { return s.second == device; });
it != audioSources_.end()) {
nhlog::ui()->debug(std::string("WebRTC: device ") +
(changed ? "changed: " : "removed: ") + "{}",
it->first);
gst_object_unref(device);
audioSources_.erase(it);
}
}
}
#endif
gboolean
newBusMessage(GstBus *bus G_GNUC_UNUSED, GstMessage *msg, gpointer user_data)
{
WebRTCSession *session = static_cast<WebRTCSession *>(user_data);
switch (GST_MESSAGE_TYPE(msg)) {
#if GST_CHECK_VERSION(1, 18, 0)
case GST_MESSAGE_DEVICE_ADDED: {
GstDevice *device;
gst_message_parse_device_added(msg, &device);
addDevice(device);
break;
}
case GST_MESSAGE_DEVICE_REMOVED: {
GstDevice *device;
gst_message_parse_device_removed(msg, &device);
removeDevice(device, false);
break;
}
case GST_MESSAGE_DEVICE_CHANGED: {
GstDevice *device;
GstDevice *oldDevice;
gst_message_parse_device_changed(msg, &device, &oldDevice);
removeDevice(oldDevice, true);
addDevice(device);
break;
}
#endif
case GST_MESSAGE_EOS:
nhlog::ui()->error("WebRTC: end of stream");
session->end();
@ -176,7 +234,7 @@ createAnswer(GstPromise *promise, gpointer webrtc)
g_signal_emit_by_name(webrtc, "create-answer", nullptr, promise);
}
#if GST_CHECK_VERSION(1, 17, 0)
#if GST_CHECK_VERSION(1, 18, 0)
void
iceGatheringStateChanged(GstElement *webrtc,
GParamSpec *pspec G_GNUC_UNUSED,
@ -223,7 +281,7 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
{
nhlog::ui()->debug("WebRTC: local candidate: (m-line:{}):{}", mlineIndex, candidate);
#if GST_CHECK_VERSION(1, 17, 0)
#if GST_CHECK_VERSION(1, 18, 0)
localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
return;
#else
@ -233,8 +291,10 @@ addLocalICECandidate(GstElement *webrtc G_GNUC_UNUSED,
return;
}
localcandidates_.push_back({"audio", (uint16_t)mlineIndex, candidate});
// GStreamer v1.16: webrtcbin's notify::ice-gathering-state triggers
// GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early. Fixed in v1.17.
// GST_WEBRTC_ICE_GATHERING_STATE_COMPLETE too early. Fixed in v1.18.
// Use a 100ms timeout in the meantime
static guint timerid = 0;
if (timerid)
@ -423,8 +483,12 @@ WebRTCSession::acceptICECandidates(
for (const auto &c : candidates) {
nhlog::ui()->debug(
"WebRTC: remote candidate: (m-line:{}):{}", c.sdpMLineIndex, c.candidate);
g_signal_emit_by_name(
webrtc_, "add-ice-candidate", c.sdpMLineIndex, c.candidate.c_str());
if (!c.candidate.empty()) {
g_signal_emit_by_name(webrtc_,
"add-ice-candidate",
c.sdpMLineIndex,
c.candidate.c_str());
}
}
}
}
@ -471,7 +535,7 @@ WebRTCSession::startPipeline(int opusPayloadType)
gst_element_set_state(pipe_, GST_STATE_READY);
g_signal_connect(webrtc_, "pad-added", G_CALLBACK(addDecodeBin), pipe_);
#if GST_CHECK_VERSION(1, 17, 0)
#if GST_CHECK_VERSION(1, 18, 0)
// capture ICE gathering completion
g_signal_connect(
webrtc_, "notify::ice-gathering-state", G_CALLBACK(iceGatheringStateChanged), nullptr);
@ -488,7 +552,7 @@ WebRTCSession::startPipeline(int opusPayloadType)
}
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipe_));
gst_bus_add_watch(bus, newBusMessage, this);
busWatchId_ = gst_bus_add_watch(bus, newBusMessage, this);
gst_object_unref(bus);
emit stateChanged(State::INITIATED);
return true;
@ -497,19 +561,18 @@ WebRTCSession::startPipeline(int opusPayloadType)
bool
WebRTCSession::createPipeline(int opusPayloadType)
{
int nSources = audioSources_ ? g_list_length(audioSources_) : 0;
if (nSources == 0) {
if (audioSources_.empty()) {
nhlog::ui()->error("WebRTC: no audio sources");
return false;
}
if (audioSourceIndex_ < 0 || audioSourceIndex_ >= nSources) {
if (audioSourceIndex_ < 0 || (size_t)audioSourceIndex_ >= audioSources_.size()) {
nhlog::ui()->error("WebRTC: invalid audio source index");
return false;
}
GstElement *source = gst_device_create_element(
GST_DEVICE_CAST(g_list_nth_data(audioSources_, audioSourceIndex_)), nullptr);
GstElement *source =
gst_device_create_element(audioSources_[audioSourceIndex_].second, nullptr);
GstElement *volume = gst_element_factory_make("volume", "srclevel");
GstElement *convert = gst_element_factory_make("audioconvert", nullptr);
GstElement *resample = gst_element_factory_make("audioresample", nullptr);
@ -594,12 +657,40 @@ WebRTCSession::end()
gst_element_set_state(pipe_, GST_STATE_NULL);
gst_object_unref(pipe_);
pipe_ = nullptr;
g_source_remove(busWatchId_);
busWatchId_ = 0;
}
webrtc_ = nullptr;
if (state_ != State::DISCONNECTED)
emit stateChanged(State::DISCONNECTED);
}
#if GST_CHECK_VERSION(1, 18, 0)
void
WebRTCSession::startDeviceMonitor()
{
if (!initialised_)
return;
static GstDeviceMonitor *monitor = nullptr;
if (!monitor) {
monitor = gst_device_monitor_new();
GstCaps *caps = gst_caps_new_empty_simple("audio/x-raw");
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
gst_caps_unref(caps);
GstBus *bus = gst_device_monitor_get_bus(monitor);
gst_bus_add_watch(bus, newBusMessage, nullptr);
gst_object_unref(bus);
if (!gst_device_monitor_start(monitor)) {
nhlog::ui()->error("WebRTC: failed to start device monitor");
return;
}
}
}
#else
void
WebRTCSession::refreshDevices()
{
@ -613,31 +704,42 @@ WebRTCSession::refreshDevices()
gst_device_monitor_add_filter(monitor, "Audio/Source", caps);
gst_caps_unref(caps);
}
g_list_free_full(audioSources_, g_object_unref);
audioSources_ = gst_device_monitor_get_devices(monitor);
std::for_each(audioSources_.begin(), audioSources_.end(), [](const auto &s) {
gst_object_unref(s.second);
});
audioSources_.clear();
GList *devices = gst_device_monitor_get_devices(monitor);
if (devices) {
audioSources_.reserve(g_list_length(devices));
for (GList *l = devices; l != nullptr; l = l->next)
addDevice(GST_DEVICE_CAST(l->data));
g_list_free(devices);
}
}
#endif
std::vector<std::string>
WebRTCSession::getAudioSourceNames(const std::string &defaultDevice)
{
if (!initialised_)
return {};
#if !GST_CHECK_VERSION(1, 18, 0)
refreshDevices();
#endif
// move default device to top of the list
if (auto it = std::find_if(audioSources_.begin(),
audioSources_.end(),
[&](const auto &s) { return s.first == defaultDevice; });
it != audioSources_.end())
std::swap(audioSources_.front(), *it);
std::vector<std::string> ret;
ret.reserve(g_list_length(audioSources_));
for (GList *l = audioSources_; l != nullptr; l = l->next) {
gchar *name = gst_device_get_display_name(GST_DEVICE_CAST(l->data));
ret.emplace_back(name);
g_free(name);
if (ret.back() == defaultDevice) {
// move default device to top of the list
std::swap(audioSources_->data, l->data);
std::swap(ret.front(), ret.back());
}
}
ret.reserve(audioSources_.size());
std::for_each(audioSources_.cbegin(), audioSources_.cend(), [&](const auto &s) {
ret.push_back(s.first);
});
return ret;
}
#else
bool
@ -688,6 +790,10 @@ void
WebRTCSession::refreshDevices()
{}
void
WebRTCSession::startDeviceMonitor()
{}
std::vector<std::string>
WebRTCSession::getAudioSourceNames(const std::string &)
{

View file

@ -7,7 +7,6 @@
#include "mtx/events/voip.hpp"
typedef struct _GList GList;
typedef struct _GstElement GstElement;
class WebRTCSession : public QObject
@ -64,18 +63,19 @@ private slots:
private:
WebRTCSession();
bool initialised_ = false;
State state_ = State::DISCONNECTED;
GstElement *pipe_ = nullptr;
GstElement *webrtc_ = nullptr;
bool initialised_ = false;
State state_ = State::DISCONNECTED;
GstElement *pipe_ = nullptr;
GstElement *webrtc_ = nullptr;
unsigned int busWatchId_ = 0;
std::string stunServer_;
std::vector<std::string> turnServers_;
GList *audioSources_ = nullptr;
int audioSourceIndex_ = -1;
bool startPipeline(int opusPayloadType);
bool createPipeline(int opusPayloadType);
void refreshDevices();
void startDeviceMonitor();
public:
WebRTCSession(WebRTCSession const &) = delete;

View file

@ -573,7 +573,7 @@ EventStore::decryptEvent(const IdIndex &idx,
room_id_,
index.sender_key);
dummy.content.body =
tr("-- Reply attack! This message index was reused! --").toStdString();
tr("-- Replay attack! This message index was reused! --").toStdString();
break;
case olm::DecryptionErrorCode::UnknownFingerprint:
// TODO: don't fail, just show in UI.

View file

@ -565,6 +565,25 @@ TimelineModel::fetchMore(const QModelIndex &)
events.fetchMore();
}
void
TimelineModel::syncState(const mtx::responses::State &s)
{
using namespace mtx::events;
for (const auto &e : s.events) {
if (std::holds_alternative<StateEvent<state::Avatar>>(e))
emit roomAvatarUrlChanged();
else if (std::holds_alternative<StateEvent<state::Name>>(e))
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
}
}
}
void
TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
{
@ -574,6 +593,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
events.handleSync(timeline);
using namespace mtx::events;
for (auto e : timeline.events) {
if (auto encryptedEvent = std::get_if<EncryptedEvent<msg::Encrypted>>(&e)) {
MegolmSessionIndex index;
@ -597,6 +617,16 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
emit newCallEvent(event);
},
e);
else if (std::holds_alternative<StateEvent<state::Avatar>>(e))
emit roomAvatarUrlChanged();
else if (std::holds_alternative<StateEvent<state::Name>>(e))
emit roomNameChanged();
else if (std::holds_alternative<StateEvent<state::Topic>>(e))
emit roomTopicChanged();
else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
emit roomAvatarUrlChanged();
emit roomNameChanged();
}
}
updateLastMessage();
}
@ -737,12 +767,6 @@ TimelineModel::formatDateSeparator(QDate date) const
return date.toString(fmt);
}
QString
TimelineModel::escapeEmoji(QString str) const
{
return utils::replaceEmoji(str);
}
void
TimelineModel::viewRawMessage(QString id) const
{
@ -1440,7 +1464,7 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, QColor bg)
QStringList uidWithoutLast;
auto formatUser = [this, bg](const QString &user_id) -> QString {
auto uncoloredUsername = escapeEmoji(displayName(user_id));
auto uncoloredUsername = utils::replaceEmoji(displayName(user_id));
QString prefix =
QString("<font color=\"%1\">").arg(manager_->userColor(user_id, bg).name());
@ -1490,7 +1514,7 @@ TimelineModel::formatJoinRuleEvent(QString id)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.join_rule) {
case mtx::events::state::JoinRule::Public:
@ -1515,7 +1539,7 @@ TimelineModel::formatGuestAccessEvent(QString id)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.guest_access) {
case mtx::events::state::AccessState::CanJoin:
@ -1540,7 +1564,7 @@ TimelineModel::formatHistoryVisibilityEvent(QString id)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
QString name = utils::replaceEmoji(displayName(user));
switch (event->content.history_visibility) {
case mtx::events::state::Visibility::WorldReadable:
@ -1573,7 +1597,7 @@ TimelineModel::formatPowerLevelEvent(QString id)
return "";
QString user = QString::fromStdString(event->sender);
QString name = escapeEmoji(displayName(user));
QString name = utils::replaceEmoji(displayName(user));
// TODO: power levels rendering is actually a bit complex. work on this later.
return tr("%1 has changed the room's permissions.").arg(name);
@ -1602,7 +1626,7 @@ TimelineModel::formatMemberEvent(QString id)
}
QString user = QString::fromStdString(event->state_key);
QString name = escapeEmoji(displayName(user));
QString name = utils::replaceEmoji(displayName(user));
QString rendered;
// see table https://matrix.org/docs/spec/client_server/latest#m-room-member
@ -1675,3 +1699,37 @@ TimelineModel::formatMemberEvent(QString id)
return rendered;
}
QString
TimelineModel::roomName() const
{
auto info = cache::getRoomInfo({room_id_.toStdString()});
if (!info.count(room_id_))
return "";
else
return QString::fromStdString(info[room_id_].name);
}
QString
TimelineModel::roomAvatarUrl() const
{
auto info = cache::getRoomInfo({room_id_.toStdString()});
if (!info.count(room_id_))
return "";
else
return QString::fromStdString(info[room_id_].avatar_url);
}
QString
TimelineModel::roomTopic() const
{
auto info = cache::getRoomInfo({room_id_.toStdString()});
if (!info.count(room_id_))
return "";
else
return utils::replaceEmoji(utils::linkifyMessage(
utils::escapeBlacklistedHtml(QString::fromStdString(info[room_id_].topic))));
}

View file

@ -146,6 +146,9 @@ class TimelineModel : public QAbstractListModel
Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply)
Q_PROPERTY(
bool paginationInProgress READ paginationInProgress NOTIFY paginationInProgressChanged)
Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
public:
explicit TimelineModel(TimelineViewManager *manager,
@ -203,7 +206,6 @@ public:
Q_INVOKABLE QString formatGuestAccessEvent(QString id);
Q_INVOKABLE QString formatPowerLevelEvent(QString id);
Q_INVOKABLE QString escapeEmoji(QString str) const;
Q_INVOKABLE void viewRawMessage(QString id) const;
Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
Q_INVOKABLE void openUserProfile(QString userid);
@ -226,6 +228,7 @@ public:
void updateLastMessage();
void addEvents(const mtx::responses::Timeline &events);
void syncState(const mtx::responses::State &state);
template<class T>
void sendMessageEvent(const T &content, mtx::events::EventType eventType);
RelatedInfo relatedInfo(QString id);
@ -262,6 +265,11 @@ public slots:
void setDecryptDescription(bool decrypt) { decryptDescription = decrypt; }
void clearTimeline() { events.clearTimeline(); }
QString roomName() const;
QString roomTopic() const;
QString roomAvatarUrl() const;
QString roomId() const { return room_id_; }
private slots:
void addPendingMessage(mtx::events::collections::TimelineEvents event);
@ -282,6 +290,10 @@ signals:
void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
void updateFlowEventId(std::string event_id);
void roomNameChanged();
void roomTopicChanged();
void roomAvatarUrlChanged();
private:
template<typename T>
void sendEncryptedMessage(mtx::events::RoomEvent<T> msg, mtx::events::EventType eventType);

View file

@ -13,6 +13,7 @@
#include "ColorImageProvider.h"
#include "DelegateChooser.h"
#include "Logging.h"
#include "MainWindow.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
@ -102,7 +103,7 @@ TimelineViewManager::userStatus(QString id) const
TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettings,
CallManager *callManager,
QWidget *parent)
ChatPage *parent)
: imgProvider(new MxcImageProvider())
, colorImgProvider(new ColorImageProvider())
, blurhashProvider(new BlurhashProvider())
@ -189,11 +190,8 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
view->engine()->addImageProvider("blurhash", blurhashProvider);
view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
connect(dynamic_cast<ChatPage *>(parent),
&ChatPage::themeChanged,
this,
&TimelineViewManager::updateColorPalette);
connect(dynamic_cast<ChatPage *>(parent),
connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
connect(parent,
&ChatPage::decryptSidebarChanged,
this,
&TimelineViewManager::updateEncryptedDescriptions);
@ -295,7 +293,7 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
}
}
});
connect(dynamic_cast<ChatPage *>(parent), &ChatPage::loggedOut, this, [this]() {
connect(parent, &ChatPage::loggedOut, this, [this]() {
isInitialSync_ = true;
emit initialSyncChanged(true);
});
@ -313,6 +311,7 @@ TimelineViewManager::sync(const mtx::responses::Rooms &rooms)
&TimelineModel::newCallEvent,
callManager_,
&CallManager::syncEvent);
room_model->syncState(room.state);
room_model->addEvents(room.timeline);
if (!isInitialSync_)
disconnect(room_model.data(),
@ -363,6 +362,12 @@ TimelineViewManager::setHistoryView(const QString &room_id)
}
}
QString
TimelineViewManager::escapeEmoji(QString str) const
{
return utils::replaceEmoji(str);
}
void
TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
{
@ -401,6 +406,28 @@ TimelineViewManager::openLink(QString link) const
QDesktopServices::openUrl(link);
}
void
TimelineViewManager::openInviteUsersDialog()
{
MainWindow::instance()->openInviteUsersDialog(
[this](const QStringList &invitees) { emit inviteUsers(invitees); });
}
void
TimelineViewManager::openMemberListDialog() const
{
MainWindow::instance()->openMemberListDialog(timeline_->roomId());
}
void
TimelineViewManager::openLeaveRoomDialog() const
{
MainWindow::instance()->openLeaveRoomDialog(timeline_->roomId());
}
void
TimelineViewManager::openRoomSettings() const
{
MainWindow::instance()->openRoomSettings(timeline_->roomId());
}
void
TimelineViewManager::updateReadReceipts(const QString &room_id,
const std::vector<QString> &event_ids)

View file

@ -22,6 +22,7 @@ class BlurhashProvider;
class CallManager;
class ColorImageProvider;
class UserSettings;
class ChatPage;
class DeviceVerificationList : public QObject
{
@ -45,11 +46,13 @@ class TimelineViewManager : public QObject
TimelineModel *timeline MEMBER timeline_ READ activeTimeline NOTIFY activeTimelineChanged)
Q_PROPERTY(
bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
Q_PROPERTY(
bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged)
public:
TimelineViewManager(QSharedPointer<UserSettings> userSettings,
CallManager *callManager,
QWidget *parent = nullptr);
ChatPage *parent = nullptr);
QWidget *getWidget() const { return container; }
void sync(const mtx::responses::Rooms &rooms);
@ -59,14 +62,21 @@ public:
Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
bool isNarrowView() const { return isNarrowView_; }
Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
Q_INVOKABLE QColor userColor(QString id, QColor background);
Q_INVOKABLE QString escapeEmoji(QString str) const;
Q_INVOKABLE QString userPresence(QString id) const;
Q_INVOKABLE QString userStatus(QString id) const;
Q_INVOKABLE void openLink(QString link) const;
Q_INVOKABLE void openInviteUsersDialog();
Q_INVOKABLE void openMemberListDialog() const;
Q_INVOKABLE void openLeaveRoomDialog() const;
Q_INVOKABLE void openRoomSettings() const;
signals:
void clearRoomMessageCount(QString roomid);
void updateRoomsLastMessage(QString roomid, const DescInfo &info);
@ -79,6 +89,9 @@ signals:
QString userId,
QString deviceId,
bool isRequest = false);
void inviteUsers(QStringList users);
void showRoomList();
void narrowViewChanged();
public slots:
void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
@ -128,6 +141,23 @@ public slots:
timeline_->clearTimeline();
}
void enableBackButton()
{
if (isNarrowView_)
return;
isNarrowView_ = true;
emit narrowViewChanged();
}
void disableBackButton()
{
if (!isNarrowView_)
return;
isNarrowView_ = false;
emit narrowViewChanged();
}
void backToRooms() { emit showRoomList(); }
private:
#ifdef USE_QUICK_VIEW
QQuickView *view;
@ -145,6 +175,7 @@ private:
CallManager *callManager_ = nullptr;
bool isInitialSync_ = true;
bool isNarrowView_ = false;
QSharedPointer<UserSettings> settings;
QHash<QString, QColor> userColors;