From 3bf111a0914b5afc9bcacfa3d4a762be763ee88a Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 23 Jul 2017 14:16:13 +0200 Subject: [PATCH] More progress on tab-complete There are missing parts to the "account-based" emotes that needs to be completed before emote completion can be considered done. For now, when I've been testing, I've been manually injecting the oauthClient and oauthToken to the settings file with the `user_subscriptions` scope --- src/accountmanager.cpp | 5 +++ src/application.cpp | 3 +- src/completionmanager.cpp | 52 +++++++++++++++++++++- src/completionmanager.hpp | 32 ++++++-------- src/emotemanager.cpp | 78 ++++++++++++++++++++++++++++++++- src/emotemanager.hpp | 27 ++++++++++-- src/ircmanager.cpp | 31 +------------ src/ircmanager.hpp | 2 - src/widgets/chatwidget.cpp | 1 + src/widgets/chatwidget.hpp | 5 ++- src/widgets/chatwidgetinput.cpp | 5 ++- src/widgets/mainwindow.cpp | 4 +- src/widgets/mainwindow.hpp | 7 ++- src/widgets/notebook.cpp | 4 +- src/widgets/notebook.hpp | 10 +++-- src/widgets/notebookpage.cpp | 1 + src/widgets/notebookpage.hpp | 2 + src/windowmanager.cpp | 7 ++- src/windowmanager.hpp | 5 ++- 19 files changed, 212 insertions(+), 69 deletions(-) diff --git a/src/accountmanager.cpp b/src/accountmanager.cpp index 4dcfdd67c..c96831422 100644 --- a/src/accountmanager.cpp +++ b/src/accountmanager.cpp @@ -1,5 +1,7 @@ #include "accountmanager.hpp" +#include + namespace chatterino { namespace { @@ -25,6 +27,9 @@ AccountManager::AccountManager() if (!envUsername.isEmpty() && !envOauthToken.isEmpty()) { this->addTwitchUser(twitch::TwitchUser(envUsername, envOauthToken, "")); } + + pajlada::Settings::Setting::set( + "/accounts/current/roomID", "11148817", pajlada::Settings::SettingOption::DoNotWriteToJSON); } twitch::TwitchUser &AccountManager::getTwitchAnon() diff --git a/src/application.cpp b/src/application.cpp index 7d4d8bce7..254a006db 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -9,7 +9,8 @@ namespace chatterino { // It will create the instances of the major classes, and connect their signals to each other Application::Application() - : windowManager(this->channelManager, this->colorScheme) + : completionManager(this->emoteManager) + , windowManager(this->channelManager, this->colorScheme, this->completionManager) , colorScheme(this->windowManager) , emoteManager(this->windowManager, this->resources) , resources(this->emoteManager, this->windowManager) diff --git a/src/completionmanager.cpp b/src/completionmanager.cpp index ff33f202c..e040adad1 100644 --- a/src/completionmanager.cpp +++ b/src/completionmanager.cpp @@ -1,8 +1,11 @@ #include "completionmanager.hpp" +#include "common.hpp" +#include "emotemanager.hpp" namespace chatterino { -CompletionManager::CompletionManager() +CompletionManager::CompletionManager(EmoteManager &_emoteManager) + : emoteManager(_emoteManager) { } @@ -10,11 +13,58 @@ CompletionModel *CompletionManager::createModel(const std::string &channelName) { CompletionModel *ret = new CompletionModel(); + this->updateModel(ret, channelName); + + this->emoteManager.bttvGlobalEmoteCodes.updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.ffzGlobalEmoteCodes.updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.bttvChannelEmoteCodes[channelName].updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + + this->emoteManager.ffzChannelEmoteCodes[channelName].updated.connect([=]() { + this->updateModel(ret, channelName); // + }); + return ret; } void CompletionManager::updateModel(CompletionModel *model, const std::string &channelName) { + model->emotes.clear(); + + for (const auto &m : this->emoteManager.twitchAccountEmotes) { + for (const auto &emoteName : m.second.emoteCodes) { + model->emotes.push_back(qS(emoteName)); + } + } + + std::vector &bttvGlobalEmoteCodes = this->emoteManager.bttvGlobalEmoteCodes; + for (const auto &m : bttvGlobalEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &ffzGlobalEmoteCodes = this->emoteManager.ffzGlobalEmoteCodes; + for (const auto &m : ffzGlobalEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &bttvChannelEmoteCodes = + this->emoteManager.bttvChannelEmoteCodes[channelName]; + for (const auto &m : bttvChannelEmoteCodes) { + model->emotes.push_back(qS(m)); + } + + std::vector &ffzChannelEmoteCodes = + this->emoteManager.ffzChannelEmoteCodes[channelName]; + for (const auto &m : ffzChannelEmoteCodes) { + model->emotes.push_back(qS(m)); + } } } // namespace chatterino diff --git a/src/completionmanager.hpp b/src/completionmanager.hpp index 191a4c670..29980ac00 100644 --- a/src/completionmanager.hpp +++ b/src/completionmanager.hpp @@ -1,12 +1,15 @@ #pragma once -#include +#include +#include #include namespace chatterino { -class CompletionModel : public QAbstractItemModel +class EmoteManager; + +class CompletionModel : public QAbstractListModel { public: virtual int columnCount(const QModelIndex & /*parent*/) const override @@ -16,34 +19,27 @@ public: virtual QVariant data(const QModelIndex &index, int role) const override { - // TODO: Implement - return QVariant(); - } - - virtual QModelIndex index(int row, int column, const QModelIndex &parent) const override - { - // TODO: Implement - return QModelIndex(); - } - - virtual QModelIndex parent(const QModelIndex &child) const override - { - return QModelIndex(); + // TODO: Implement more safely + return QVariant(this->emotes.at(index.row())); } virtual int rowCount(const QModelIndex &parent) const override { - return 1; + return this->emotes.size(); } + + QVector emotes; }; class CompletionManager { - CompletionManager(); + CompletionManager(EmoteManager &_emoteManager); + + EmoteManager &emoteManager; public: CompletionModel *createModel(const std::string &channelName); - void updateModel(CompletionModel *model, const std::string &channelName); + void updateModel(CompletionModel *model, const std::string &channelName = std::string()); friend class Application; }; diff --git a/src/emotemanager.cpp b/src/emotemanager.cpp index 438f00ae5..eded4ed5a 100644 --- a/src/emotemanager.cpp +++ b/src/emotemanager.cpp @@ -1,4 +1,5 @@ #include "emotemanager.hpp" +#include "common.hpp" #include "resources.hpp" #include "util/urlfetch.hpp" #include "windowmanager.hpp" @@ -25,6 +26,12 @@ EmoteManager::EmoteManager(WindowManager &_windowManager, Resources &_resources) , resources(_resources) { // Note: Do not use this->resources in ctor + pajlada::Settings::Setting roomID( + "/accounts/current/roomID", "", pajlada::Settings::SettingOption::DoNotWriteToJSON); + + roomID.getValueChangedSignal().connect([this](const std::string &roomID) { + this->refreshTwitchEmotes(roomID); // + }); } void EmoteManager::loadGlobalEmotes() @@ -48,6 +55,7 @@ void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName) QString linkTemplate = "https:" + rootNode.value("urlTemplate").toString(); + std::vector codes; for (const QJsonValue &emoteNode : emotesNode) { QJsonObject emoteObject = emoteNode.toObject(); @@ -67,7 +75,10 @@ void EmoteManager::reloadBTTVChannelEmotes(const QString &channelName) this->bttvChannelEmotes.insert(code, emote); channelEmoteMap.insert(code, emote); + codes.push_back(code.toStdString()); } + + this->bttvChannelEmoteCodes[channelName.toStdString()] = codes; }); } @@ -84,6 +95,7 @@ void EmoteManager::reloadFFZChannelEmotes(const QString &channelName) auto setsNode = rootNode.value("sets").toObject(); + std::vector codes; for (const QJsonValue &setNode : setsNode) { auto emotesNode = setNode.toObject().value("emoticons").toArray(); @@ -105,7 +117,10 @@ void EmoteManager::reloadFFZChannelEmotes(const QString &channelName) this->ffzChannelEmotes.insert(code, emote); channelEmoteMap.insert(code, emote); + codes.push_back(code.toStdString()); } + + this->ffzChannelEmoteCodes[channelName.toStdString()] = codes; } }); } @@ -279,9 +294,61 @@ void EmoteManager::parseEmojis(std::vector> &pars } } +void EmoteManager::refreshTwitchEmotes(const std::string &roomID) +{ + std::string oauthClient = + pajlada::Settings::Setting::get("/accounts/" + roomID + "/oauthClient"); + std::string oauthToken = + pajlada::Settings::Setting::get("/accounts/" + roomID + "/oauthToken"); + + TwitchAccountEmoteData &emoteData = this->twitchAccountEmotes[roomID]; + + if (emoteData.filled) { + qDebug() << "Already loaded for room id " << qS(roomID); + return; + } + + qDebug() << "Loading emotes for room id " << qS(roomID); + + if (oauthClient.empty() || oauthToken.empty()) { + qDebug() << "Missing oauth client/token"; + return; + } + + // api:v5 + QString url("https://api.twitch.tv/kraken/users/" + qS(roomID) + + "/emotes?api_version=5&oauth_token=" + qS(oauthToken) + + "&client_id=" + qS(oauthClient)); + + qDebug() << url; + + util::urlFetchJSONTimeout( + url, + [=, &emoteData](QJsonObject &root) { + emoteData.emoteSets.clear(); + emoteData.emoteCodes.clear(); + + auto emoticonSets = root.value("emoticon_sets").toObject(); + for (QJsonObject::iterator it = emoticonSets.begin(); it != emoticonSets.end(); ++it) { + std::string emoteSetString = it.key().toStdString(); + QJsonArray emoteSetList = it.value().toArray(); + + for (QJsonValue emoteValue : emoteSetList) { + QJsonObject emoticon = emoteValue.toObject(); + std::string id = emoticon["id"].toString().toStdString(); + std::string code = emoticon["code"].toString().toStdString(); + emoteData.emoteSets[emoteSetString].push_back({id, code}); + emoteData.emoteCodes.push_back(code); + } + } + + emoteData.filled = true; + }, + 3000); +} + void EmoteManager::loadBTTVEmotes() { - // bttv QNetworkAccessManager *manager = new QNetworkAccessManager(); QUrl url("https://api.betterttv.net/2/emotes"); @@ -299,6 +366,7 @@ void EmoteManager::loadBTTVEmotes() QString linkTemplate = "https:" + root.value("urlTemplate").toString(); + std::vector codes; for (const QJsonValue &emote : emotes) { QString id = emote.toObject().value("id").toString(); QString code = emote.toObject().value("code").toString(); @@ -311,7 +379,10 @@ void EmoteManager::loadBTTVEmotes() this->bttvGlobalEmotes.insert( code, new LazyLoadedImage(*this, this->windowManager, url, 1, code, code + "\nGlobal BTTV Emote")); + codes.push_back(code.toStdString()); } + + this->bttvGlobalEmoteCodes = codes; } reply->deleteLater(); @@ -321,7 +392,6 @@ void EmoteManager::loadBTTVEmotes() void EmoteManager::loadFFZEmotes() { - // ffz QNetworkAccessManager *manager = new QNetworkAccessManager(); QUrl url("https://api.frankerfacez.com/v1/set/global"); @@ -337,6 +407,7 @@ void EmoteManager::loadFFZEmotes() auto sets = root.value("sets").toObject(); + std::vector codes; for (const QJsonValue &set : sets) { auto emoticons = set.toObject().value("emoticons").toArray(); @@ -354,7 +425,10 @@ void EmoteManager::loadFFZEmotes() this->ffzGlobalEmotes.insert( code, new LazyLoadedImage(*this, this->windowManager, url1, 1, code, code + "\nGlobal FFZ Emote")); + codes.push_back(code.toStdString()); } + + this->ffzGlobalEmoteCodes = codes; } } diff --git a/src/emotemanager.hpp b/src/emotemanager.hpp index 1cce56a2b..7324ce67e 100644 --- a/src/emotemanager.hpp +++ b/src/emotemanager.hpp @@ -5,6 +5,7 @@ #include "concurrentmap.hpp" #include "emojis.hpp" #include "messages/lazyloadedimage.hpp" +#include "signalvector.hpp" #include "twitch/emotevalue.hpp" #include @@ -88,11 +89,26 @@ private: public: void parseEmojis(std::vector> &parsedWords, const QString &text); -private: /// Twitch emotes - // username emote code - ConcurrentStdMap> twitchAccountEmotes; + void refreshTwitchEmotes(const std::string &roomID); + struct TwitchAccountEmoteData { + struct TwitchEmote { + std::string id; + std::string code; + }; + + // emote set + std::map> emoteSets; + + std::vector emoteCodes; + + bool filled = false; + }; + + std::map twitchAccountEmotes; + +private: // emote code ConcurrentMap _twitchEmotes; @@ -105,6 +121,9 @@ private: public: ConcurrentMap bttvChannels; EmoteMap bttvGlobalEmotes; + SignalVector bttvGlobalEmoteCodes; + // roomID + std::map> bttvChannelEmoteCodes; EmoteMap _bttvChannelEmoteFromCaches; private: @@ -116,6 +135,8 @@ private: public: ConcurrentMap ffzChannels; EmoteMap ffzGlobalEmotes; + SignalVector ffzGlobalEmoteCodes; + std::map> ffzChannelEmoteCodes; private: ConcurrentMap _ffzChannelEmoteFromCaches; diff --git a/src/ircmanager.cpp b/src/ircmanager.cpp index 6dfbfef33..4aa3ba30c 100644 --- a/src/ircmanager.cpp +++ b/src/ircmanager.cpp @@ -79,8 +79,6 @@ Communi::IrcConnection *IrcManager::createConnection(bool doRead) this->refreshIgnoredUsers(username, oauthClient, oauthToken); } - this->refreshTwitchEmotes(username, oauthClient, oauthToken); - if (doRead) { connection->sendCommand( Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership")); @@ -130,34 +128,6 @@ void IrcManager::refreshIgnoredUsers(const QString &username, const QString &oau }); } -void IrcManager::refreshTwitchEmotes(const QString &username, const QString &oauthClient, - const QString &oauthToken) -{ - QString url("https://api.twitch.tv/kraken/users/" + username + - "/emotes?oauth_token=" + oauthToken + "&client_id=" + oauthClient); - - if (true) { - util::urlFetchJSONTimeout( - url, - [=](QJsonObject &root) { - // nextLink = - // root.value("_links").toObject().value("next").toString(); - - auto blocks = root.value("blocks").toArray(); - - _twitchBlockedUsersMutex.lock(); - for (QJsonValue block : blocks) { - QJsonObject user = block.toObject().value("user").toObject(); - // display_name - _twitchBlockedUsers.insert(user.value("name").toString().toLower(), true); - } - _twitchBlockedUsersMutex.unlock(); - qDebug() << "XD"; - }, - 3000, &this->networkAccessManager); - } -} - void IrcManager::beginConnecting() { uint32_t generation = ++this->connectionGeneration; @@ -305,6 +275,7 @@ void IrcManager::handleRoomStateMessage(Communi::IrcMessage *message) if (iterator != tags.end()) { std::string roomID = iterator.value().toString().toStdString(); + this->resources.loadChannelData(roomID); } } diff --git a/src/ircmanager.hpp b/src/ircmanager.hpp index a1f597fa0..b658ae413 100644 --- a/src/ircmanager.hpp +++ b/src/ircmanager.hpp @@ -76,8 +76,6 @@ private: void refreshIgnoredUsers(const QString &username, const QString &oauthClient, const QString &oauthToken); - void refreshTwitchEmotes(const QString &username, const QString &oauthClient, - const QString &oauthToken); void beginConnecting(); diff --git a/src/widgets/chatwidget.cpp b/src/widgets/chatwidget.cpp index 8c7b06725..92e2331a3 100644 --- a/src/widgets/chatwidget.cpp +++ b/src/widgets/chatwidget.cpp @@ -37,6 +37,7 @@ static int index = 0; ChatWidget::ChatWidget(ChannelManager &_channelManager, NotebookPage *parent) : BaseWidget(parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , channel(_channelManager.emptyChannel) , vbox(this) , header(this) diff --git a/src/widgets/chatwidget.hpp b/src/widgets/chatwidget.hpp index 607cfda53..64a038ee7 100644 --- a/src/widgets/chatwidget.hpp +++ b/src/widgets/chatwidget.hpp @@ -21,6 +21,7 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; namespace widgets { @@ -59,9 +60,11 @@ public: protected: virtual void paintEvent(QPaintEvent *) override; -private: +public: ChannelManager &channelManager; + CompletionManager &completionManager; +private: void setChannel(std::shared_ptr newChannel); void detachChannel(); diff --git a/src/widgets/chatwidgetinput.cpp b/src/widgets/chatwidgetinput.cpp index 9bc7a329e..acddf1af6 100644 --- a/src/widgets/chatwidgetinput.cpp +++ b/src/widgets/chatwidgetinput.cpp @@ -1,6 +1,7 @@ #include "widgets/chatwidgetinput.hpp" #include "chatwidget.hpp" #include "colorscheme.hpp" +#include "completionmanager.hpp" #include "ircmanager.hpp" #include "settingsmanager.hpp" @@ -45,8 +46,8 @@ ChatWidgetInput::ChatWidgetInput(ChatWidget *_chatWidget) this->refreshTheme(); this->setMessageLengthVisible(SettingsManager::getInstance().showMessageLength.get()); - // TODO: Fill in this QCompleter model using our CompletionManager - auto completer = new QCompleter(); + auto completer = new QCompleter( + this->chatWidget->completionManager.createModel(this->chatWidget->channelName)); this->textInput.setCompleter(completer); diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 3e96f3c7d..99c5bce26 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -19,10 +19,12 @@ namespace chatterino { namespace widgets { -MainWindow::MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme) +MainWindow::MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager) : BaseWidget(_colorScheme, nullptr) , channelManager(_channelManager) , colorScheme(_colorScheme) + , completionManager(_completionManager) , notebook(this->channelManager, this) , windowGeometry("/windows/0/geometry") { diff --git a/src/widgets/mainwindow.hpp b/src/widgets/mainwindow.hpp index 2a722ab51..37decd0fa 100644 --- a/src/widgets/mainwindow.hpp +++ b/src/widgets/mainwindow.hpp @@ -17,6 +17,7 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; namespace widgets { @@ -25,7 +26,8 @@ class MainWindow : public BaseWidget Q_OBJECT public: - explicit MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme); + explicit MainWindow(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager); ~MainWindow(); void layoutVisibleChatWidgets(Channel *channel = nullptr); @@ -48,6 +50,7 @@ private: ChannelManager &channelManager; ColorScheme &colorScheme; + CompletionManager &completionManager; Notebook notebook; bool loaded = false; @@ -144,6 +147,8 @@ private: }; pajlada::Settings::Setting windowGeometry; + + friend class Notebook; }; } // namespace widgets diff --git a/src/widgets/notebook.cpp b/src/widgets/notebook.cpp index f9d2ba78d..fa81ec102 100644 --- a/src/widgets/notebook.cpp +++ b/src/widgets/notebook.cpp @@ -1,5 +1,6 @@ #include "widgets/notebook.hpp" #include "colorscheme.hpp" +#include "widgets/mainwindow.hpp" #include "widgets/notebookbutton.hpp" #include "widgets/notebookpage.hpp" #include "widgets/notebooktab.hpp" @@ -18,9 +19,10 @@ namespace chatterino { namespace widgets { -Notebook::Notebook(ChannelManager &_channelManager, BaseWidget *parent) +Notebook::Notebook(ChannelManager &_channelManager, MainWindow *parent) : BaseWidget(parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , addButton(this) , settingsButton(this) , userButton(this) diff --git a/src/widgets/notebook.hpp b/src/widgets/notebook.hpp index d510584dd..2b6085334 100644 --- a/src/widgets/notebook.hpp +++ b/src/widgets/notebook.hpp @@ -12,10 +12,12 @@ namespace chatterino { class ChannelManager; -class ColorScheme; +class CompletionManager; namespace widgets { +class MainWindow; + class Notebook : public BaseWidget { Q_OBJECT @@ -23,7 +25,7 @@ class Notebook : public BaseWidget public: enum HighlightType { none, highlighted, newMessage }; - explicit Notebook(ChannelManager &_channelManager, BaseWidget *parent); + explicit Notebook(ChannelManager &_channelManager, MainWindow *parent); NotebookPage *addPage(bool select = false); @@ -50,9 +52,11 @@ public slots: void usersButtonClicked(); void addPageButtonClicked(); -private: +public: ChannelManager &channelManager; + CompletionManager &completionManager; +private: QList pages; NotebookButton addButton; diff --git a/src/widgets/notebookpage.cpp b/src/widgets/notebookpage.cpp index 7563487e4..fbec4e616 100644 --- a/src/widgets/notebookpage.cpp +++ b/src/widgets/notebookpage.cpp @@ -25,6 +25,7 @@ std::pair NotebookPage::dropPosition = std::pair(-1, -1); NotebookPage::NotebookPage(ChannelManager &_channelManager, Notebook *parent, NotebookTab *_tab) : BaseWidget(parent->colorScheme, parent) , channelManager(_channelManager) + , completionManager(parent->completionManager) , tab(_tab) , dropPreview(this) { diff --git a/src/widgets/notebookpage.hpp b/src/widgets/notebookpage.hpp index 6f8410a3c..65ad64c4d 100644 --- a/src/widgets/notebookpage.hpp +++ b/src/widgets/notebookpage.hpp @@ -18,6 +18,7 @@ namespace chatterino { class ChannelManager; +class CompletionManager; namespace widgets { @@ -29,6 +30,7 @@ public: NotebookPage(ChannelManager &_channelManager, Notebook *parent, NotebookTab *_tab); ChannelManager &channelManager; + CompletionManager &completionManager; std::pair removeFromLayout(ChatWidget *widget); void addToLayout(ChatWidget *widget, std::pair position); diff --git a/src/windowmanager.cpp b/src/windowmanager.cpp index b3d7fd594..ac0dbc9f4 100644 --- a/src/windowmanager.cpp +++ b/src/windowmanager.cpp @@ -10,9 +10,11 @@ namespace chatterino { -WindowManager::WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme) +WindowManager::WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager) : channelManager(_channelManager) , colorScheme(_colorScheme) + , completionManager(_completionManager) { } @@ -56,7 +58,8 @@ widgets::MainWindow &WindowManager::getMainWindow() std::lock_guard lock(this->windowMutex); if (this->mainWindow == nullptr) { - this->mainWindow = new widgets::MainWindow(this->channelManager, this->colorScheme); + this->mainWindow = new widgets::MainWindow(this->channelManager, this->colorScheme, + this->completionManager); } return *this->mainWindow; diff --git a/src/windowmanager.hpp b/src/windowmanager.hpp index 4cdb05520..cbf18ae23 100644 --- a/src/windowmanager.hpp +++ b/src/windowmanager.hpp @@ -8,14 +8,17 @@ namespace chatterino { class ChannelManager; class ColorScheme; +class CompletionManager; class WindowManager { public: - explicit WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme); + explicit WindowManager(ChannelManager &_channelManager, ColorScheme &_colorScheme, + CompletionManager &_completionManager); ChannelManager &channelManager; ColorScheme &colorScheme; + CompletionManager &completionManager; void layoutVisibleChatWidgets(Channel *channel = nullptr); void repaintVisibleChatWidgets(Channel *channel = nullptr);