1 #include "widgets/splits/Split.hpp"
2 
3 #include "Application.hpp"
4 #include "common/Common.hpp"
5 #include "common/Env.hpp"
6 #include "common/NetworkRequest.hpp"
7 #include "common/QLogging.hpp"
8 #include "controllers/accounts/AccountController.hpp"
9 #include "providers/twitch/EmoteValue.hpp"
10 #include "providers/twitch/TwitchChannel.hpp"
11 #include "providers/twitch/TwitchIrcServer.hpp"
12 #include "providers/twitch/TwitchMessageBuilder.hpp"
13 #include "singletons/Fonts.hpp"
14 #include "singletons/Settings.hpp"
15 #include "singletons/Theme.hpp"
16 #include "singletons/WindowManager.hpp"
17 #include "util/Clipboard.hpp"
18 #include "util/NuulsUploader.hpp"
19 #include "util/Shortcut.hpp"
20 #include "util/StreamLink.hpp"
21 #include "widgets/Notebook.hpp"
22 #include "widgets/TooltipWidget.hpp"
23 #include "widgets/Window.hpp"
24 #include "widgets/dialogs/QualityPopup.hpp"
25 #include "widgets/dialogs/SelectChannelDialog.hpp"
26 #include "widgets/dialogs/SelectChannelFiltersDialog.hpp"
27 #include "widgets/dialogs/UserInfoPopup.hpp"
28 #include "widgets/helper/ChannelView.hpp"
29 #include "widgets/helper/DebugPopup.hpp"
30 #include "widgets/helper/NotebookTab.hpp"
31 #include "widgets/helper/ResizingTextEdit.hpp"
32 #include "widgets/helper/SearchPopup.hpp"
33 #include "widgets/splits/SplitContainer.hpp"
34 #include "widgets/splits/SplitHeader.hpp"
35 #include "widgets/splits/SplitInput.hpp"
36 #include "widgets/splits/SplitOverlay.hpp"
37 
38 #include <QApplication>
39 #include <QClipboard>
40 #include <QDesktopServices>
41 #include <QDockWidget>
42 #include <QDrag>
43 #include <QJsonArray>
44 #include <QLabel>
45 #include <QListWidget>
46 #include <QMimeData>
47 #include <QMovie>
48 #include <QPainter>
49 #include <QVBoxLayout>
50 
51 #include <functional>
52 #include <random>
53 
54 namespace chatterino {
55 namespace {
showTutorialVideo(QWidget * parent,const QString & source,const QString & title,const QString & description)56     void showTutorialVideo(QWidget *parent, const QString &source,
57                            const QString &title, const QString &description)
58     {
59         auto window =
60             new BasePopup(BaseWindow::Flags::EnableCustomFrame, parent);
61         window->setWindowTitle("Chatterino - " + title);
62         window->setAttribute(Qt::WA_DeleteOnClose);
63         auto layout = new QVBoxLayout();
64         layout->addWidget(new QLabel(description));
65         auto label = new QLabel(window);
66         layout->addWidget(label);
67         auto movie = new QMovie(label);
68         movie->setFileName(source);
69         label->setMovie(movie);
70         movie->start();
71         window->getLayoutContainer()->setLayout(layout);
72         window->show();
73     }
74 }  // namespace
75 
76 pajlada::Signals::Signal<Qt::KeyboardModifiers> Split::modifierStatusChanged;
77 Qt::KeyboardModifiers Split::modifierStatus = Qt::NoModifier;
78 
Split(QWidget * parent)79 Split::Split(QWidget *parent)
80     : BaseWidget(parent)
81     , channel_(Channel::getEmpty())
82     , vbox_(new QVBoxLayout(this))
83     , header_(new SplitHeader(this))
84     , view_(new ChannelView(this))
85     , input_(new SplitInput(this))
86     , overlay_(new SplitOverlay(this))
87 {
88     this->setMouseTracking(true);
89     this->view_->setPausable(true);
90     this->view_->setFocusPolicy(Qt::FocusPolicy::NoFocus);
91 
92     this->vbox_->setSpacing(0);
93     this->vbox_->setMargin(1);
94 
95     this->vbox_->addWidget(this->header_);
96     this->vbox_->addWidget(this->view_, 1);
97     this->vbox_->addWidget(this->input_);
98 
99     // Initialize chat widget-wide hotkeys
100     // CTRL+W: Close Split
101     createShortcut(this, "CTRL+W", &Split::deleteFromContainer);
102 
103     // CTRL+R: Change Channel
104     createShortcut(this, "CTRL+R", &Split::changeChannel);
105 
106     // CTRL+F: Search
107     createShortcut(this, "CTRL+F", &Split::showSearch);
108 
109     // F5: reload emotes
110     createShortcut(this, "F5", &Split::reloadChannelAndSubscriberEmotes);
111 
112     // CTRL+F5: reconnect
113     createShortcut(this, "CTRL+F5", &Split::reconnect);
114 
115     // Alt+X: create clip LUL
116     createShortcut(this, "Alt+X", [this] {
117         if (const auto type = this->getChannel()->getType();
118             type != Channel::Type::Twitch &&
119             type != Channel::Type::TwitchWatching)
120         {
121             return;
122         }
123 
124         auto *twitchChannel =
125             dynamic_cast<TwitchChannel *>(this->getChannel().get());
126 
127         twitchChannel->createClip();
128     });
129 
130     // F10
131     createShortcut(this, "F10", [] {
132         auto *popup = new DebugPopup;
133         popup->setAttribute(Qt::WA_DeleteOnClose);
134         popup->setWindowTitle("Chatterino - Debug popup");
135         popup->show();
136     });
137 
138     // xd
139     // CreateShortcut(this, "ALT+SHIFT+RIGHT", &Split::doIncFlexX);
140     // CreateShortcut(this, "ALT+SHIFT+LEFT", &Split::doDecFlexX);
141     // CreateShortcut(this, "ALT+SHIFT+UP", &Split::doIncFlexY);
142     // CreateShortcut(this, "ALT+SHIFT+DOWN", &Split::doDecFlexY);
143 
144     this->input_->ui_.textEdit->installEventFilter(parent);
145 
146     // update placeheolder text on Twitch account change and channel change
147     this->signalHolder_.managedConnect(
148         getApp()->accounts->twitch.currentUserChanged, [this] {
149             this->updateInputPlaceholder();
150         });
151     this->signalHolder_.managedConnect(channelChanged, [this] {
152         this->updateInputPlaceholder();
153     });
154     this->updateInputPlaceholder();
155 
156     this->view_->mouseDown.connect([this](QMouseEvent *) {
157         this->giveFocus(Qt::MouseFocusReason);
158     });
159     this->view_->selectionChanged.connect([this]() {
160         if (view_->hasSelection())
161         {
162             this->input_->clearSelection();
163         }
164     });
165 
166     this->view_->openChannelIn.connect([this](
167                                            QString twitchChannel,
168                                            FromTwitchLinkOpenChannelIn openIn) {
169         ChannelPtr channel =
170             getApp()->twitch.server->getOrAddChannel(twitchChannel);
171         switch (openIn)
172         {
173             case FromTwitchLinkOpenChannelIn::Split:
174                 this->openSplitRequested.invoke(channel);
175                 break;
176             case FromTwitchLinkOpenChannelIn::Tab:
177                 this->joinChannelInNewTab(channel);
178                 break;
179             case FromTwitchLinkOpenChannelIn::BrowserPlayer:
180                 this->openChannelInBrowserPlayer(channel);
181                 break;
182             case FromTwitchLinkOpenChannelIn::Streamlink:
183                 this->openChannelInStreamlink(twitchChannel);
184                 break;
185             default:
186                 qCWarning(chatterinoWidget)
187                     << "Unhandled \"FromTwitchLinkOpenChannelIn\" enum value: "
188                     << static_cast<int>(openIn);
189         }
190     });
191 
192     this->input_->textChanged.connect([=](const QString &newText) {
193         if (getSettings()->showEmptyInput)
194         {
195             return;
196         }
197 
198         if (newText.length() == 0)
199         {
200             this->input_->hide();
201         }
202         else if (this->input_->isHidden())
203         {
204             this->input_->show();
205         }
206     });
207 
208     getSettings()->showEmptyInput.connect(
209         [this](const bool &showEmptyInput, auto) {
210             if (!showEmptyInput && this->input_->getInputText().length() == 0)
211             {
212                 this->input_->hide();
213             }
214             else
215             {
216                 this->input_->show();
217             }
218         },
219         this->managedConnections_);
220 
221     this->header_->updateModerationModeIcon();
222     this->overlay_->hide();
223 
224     this->setSizePolicy(QSizePolicy::MinimumExpanding,
225                         QSizePolicy::MinimumExpanding);
226 
227     this->managedConnect(modifierStatusChanged, [this](Qt::KeyboardModifiers
228                                                            status) {
229         if ((status ==
230              showSplitOverlayModifiers /*|| status == showAddSplitRegions*/) &&
231             this->isMouseOver_)
232         {
233             this->overlay_->show();
234         }
235         else
236         {
237             this->overlay_->hide();
238         }
239 
240         if (getSettings()->pauseChatModifier.getEnum() != Qt::NoModifier &&
241             status == getSettings()->pauseChatModifier.getEnum())
242         {
243             this->view_->pause(PauseReason::KeyboardModifier);
244         }
245         else
246         {
247             this->view_->unpause(PauseReason::KeyboardModifier);
248         }
249     });
250 
251     this->input_->ui_.textEdit->focused.connect([this] {
252         this->focused.invoke();
253     });
254     this->input_->ui_.textEdit->focusLost.connect([this] {
255         this->focusLost.invoke();
256     });
257     this->input_->ui_.textEdit->imagePasted.connect(
258         [this](const QMimeData *source) {
259             if (!getSettings()->imageUploaderEnabled)
260                 return;
261 
262             if (getSettings()->askOnImageUpload.getValue())
263             {
264                 QMessageBox msgBox;
265                 msgBox.setText("Image upload");
266                 msgBox.setInformativeText(
267                     "You are uploading an image to a 3rd party service not in "
268                     "control of the chatterino team. You may not be able to "
269                     "remove the image from the site. Are you okay with this?");
270                 msgBox.addButton(QMessageBox::Cancel);
271                 msgBox.addButton(QMessageBox::Yes);
272                 msgBox.addButton("Yes, don't ask again", QMessageBox::YesRole);
273 
274                 msgBox.setDefaultButton(QMessageBox::Yes);
275 
276                 auto picked = msgBox.exec();
277                 if (picked == QMessageBox::Cancel)
278                 {
279                     return;
280                 }
281                 else if (picked == 0)  // don't ask again button
282                 {
283                     getSettings()->askOnImageUpload.setValue(false);
284                 }
285             }
286             upload(source, this->getChannel(), *this->input_->ui_.textEdit);
287         });
288 
289     getSettings()->imageUploaderEnabled.connect(
290         [this](const bool &val) {
291             this->setAcceptDrops(val);
292         },
293         this->managedConnections_);
294 }
295 
~Split()296 Split::~Split()
297 {
298     this->usermodeChangedConnection_.disconnect();
299     this->roomModeChangedConnection_.disconnect();
300     this->channelIDChangedConnection_.disconnect();
301     this->indirectChannelChangedConnection_.disconnect();
302 }
303 
getChannelView()304 ChannelView &Split::getChannelView()
305 {
306     return *this->view_;
307 }
308 
getInput()309 SplitInput &Split::getInput()
310 {
311     return *this->input_;
312 }
313 
updateInputPlaceholder()314 void Split::updateInputPlaceholder()
315 {
316     if (!this->getChannel()->isTwitchChannel())
317     {
318         return;
319     }
320 
321     auto user = getApp()->accounts->twitch.getCurrent();
322     QString placeholderText;
323 
324     if (user->isAnon())
325     {
326         placeholderText = "Log in to send messages...";
327     }
328     else
329     {
330         placeholderText =
331             QString("Send message as %1...")
332                 .arg(getApp()->accounts->twitch.getCurrent()->getUserName());
333     }
334 
335     this->input_->ui_.textEdit->setPlaceholderText(placeholderText);
336 }
337 
joinChannelInNewTab(ChannelPtr channel)338 void Split::joinChannelInNewTab(ChannelPtr channel)
339 {
340     auto &nb = getApp()->windows->getMainWindow().getNotebook();
341     SplitContainer *container = nb.addPage(true);
342 
343     Split *split = new Split(container);
344     split->setChannel(channel);
345     container->appendSplit(split);
346 }
347 
openChannelInBrowserPlayer(ChannelPtr channel)348 void Split::openChannelInBrowserPlayer(ChannelPtr channel)
349 {
350     if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
351     {
352         QDesktopServices::openUrl(
353             "https://player.twitch.tv/?parent=twitch.tv&channel=" +
354             twitchChannel->getName());
355     }
356 }
357 
openChannelInStreamlink(QString channelName)358 void Split::openChannelInStreamlink(QString channelName)
359 {
360     try
361     {
362         openStreamlinkForChannel(channelName);
363     }
364     catch (const Exception &ex)
365     {
366         qCWarning(chatterinoWidget)
367             << "Error in doOpenStreamlink:" << ex.what();
368     }
369 }
370 
getIndirectChannel()371 IndirectChannel Split::getIndirectChannel()
372 {
373     return this->channel_;
374 }
375 
getChannel()376 ChannelPtr Split::getChannel()
377 {
378     return this->channel_.get();
379 }
380 
setChannel(IndirectChannel newChannel)381 void Split::setChannel(IndirectChannel newChannel)
382 {
383     this->channel_ = newChannel;
384 
385     this->view_->setChannel(newChannel.get());
386 
387     this->usermodeChangedConnection_.disconnect();
388     this->roomModeChangedConnection_.disconnect();
389     this->indirectChannelChangedConnection_.disconnect();
390 
391     TwitchChannel *tc = dynamic_cast<TwitchChannel *>(newChannel.get().get());
392 
393     if (tc != nullptr)
394     {
395         this->usermodeChangedConnection_ = tc->userStateChanged.connect([this] {
396             this->header_->updateModerationModeIcon();
397             this->header_->updateRoomModes();
398         });
399 
400         this->roomModeChangedConnection_ = tc->roomModesChanged.connect([this] {
401             this->header_->updateRoomModes();
402         });
403     }
404 
405     this->indirectChannelChangedConnection_ =
406         newChannel.getChannelChanged().connect([this] {
407             QTimer::singleShot(0, [this] {
408                 this->setChannel(this->channel_);
409             });
410         });
411 
412     this->header_->updateModerationModeIcon();
413     this->header_->updateChannelText();
414     this->header_->updateRoomModes();
415 
416     if (newChannel.getType() == Channel::Type::Twitch)
417     {
418         this->header_->setViewersButtonVisible(true);
419     }
420     else
421     {
422         this->header_->setViewersButtonVisible(false);
423     }
424 
425     this->channel_.get()->displayNameChanged.connect([this] {
426         this->actionRequested.invoke(Action::RefreshTab);
427     });
428 
429     this->channelChanged.invoke();
430 
431     // Queue up save because: Split channel changed
432     getApp()->windows->queueSave();
433 }
434 
setModerationMode(bool value)435 void Split::setModerationMode(bool value)
436 {
437     this->moderationMode_ = value;
438     this->header_->updateModerationModeIcon();
439     this->view_->queueLayout();
440 }
441 
getModerationMode() const442 bool Split::getModerationMode() const
443 {
444     return this->moderationMode_;
445 }
446 
insertTextToInput(const QString & text)447 void Split::insertTextToInput(const QString &text)
448 {
449     this->input_->insertText(text);
450 }
451 
showChangeChannelPopup(const char * dialogTitle,bool empty,std::function<void (bool)> callback)452 void Split::showChangeChannelPopup(const char *dialogTitle, bool empty,
453                                    std::function<void(bool)> callback)
454 {
455     if (this->selectChannelDialog_.hasElement())
456     {
457         this->selectChannelDialog_->raise();
458 
459         return;
460     }
461 
462     auto dialog = new SelectChannelDialog(this);
463     if (!empty)
464     {
465         dialog->setSelectedChannel(this->getIndirectChannel());
466     }
467     dialog->setAttribute(Qt::WA_DeleteOnClose);
468     dialog->show();
469     dialog->closed.connect([=] {
470         if (dialog->hasSeletedChannel())
471         {
472             this->setChannel(dialog->getSelectedChannel());
473             this->actionRequested.invoke(Action::RefreshTab);
474         }
475 
476         callback(dialog->hasSeletedChannel());
477         this->selectChannelDialog_ = nullptr;
478     });
479     this->selectChannelDialog_ = dialog;
480 }
481 
updateGifEmotes()482 void Split::updateGifEmotes()
483 {
484     this->view_->queueUpdate();
485 }
486 
updateLastReadMessage()487 void Split::updateLastReadMessage()
488 {
489     this->view_->updateLastReadMessage();
490 }
491 
giveFocus(Qt::FocusReason reason)492 void Split::giveFocus(Qt::FocusReason reason)
493 {
494     this->input_->ui_.textEdit->setFocus(reason);
495 }
496 
hasFocus() const497 bool Split::hasFocus() const
498 {
499     return this->input_->ui_.textEdit->hasFocus();
500 }
501 
paintEvent(QPaintEvent *)502 void Split::paintEvent(QPaintEvent *)
503 {
504     // color the background of the chat
505     QPainter painter(this);
506 
507     painter.fillRect(this->rect(), this->theme->splits.background);
508 }
509 
mouseMoveEvent(QMouseEvent * event)510 void Split::mouseMoveEvent(QMouseEvent *event)
511 {
512     this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
513 }
514 
keyPressEvent(QKeyEvent * event)515 void Split::keyPressEvent(QKeyEvent *event)
516 {
517     this->view_->unsetCursor();
518     this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
519 }
520 
keyReleaseEvent(QKeyEvent * event)521 void Split::keyReleaseEvent(QKeyEvent *event)
522 {
523     this->view_->unsetCursor();
524     this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
525 }
526 
resizeEvent(QResizeEvent * event)527 void Split::resizeEvent(QResizeEvent *event)
528 {
529     // Queue up save because: Split resized
530     getApp()->windows->queueSave();
531 
532     BaseWidget::resizeEvent(event);
533 
534     this->overlay_->setGeometry(this->rect());
535 }
536 
enterEvent(QEvent * event)537 void Split::enterEvent(QEvent *event)
538 {
539     this->isMouseOver_ = true;
540 
541     this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
542 
543     if (modifierStatus ==
544         showSplitOverlayModifiers /*|| modifierStatus == showAddSplitRegions*/)
545     {
546         this->overlay_->show();
547     }
548 
549     this->actionRequested.invoke(Action::ResetMouseStatus);
550 }
551 
leaveEvent(QEvent * event)552 void Split::leaveEvent(QEvent *event)
553 {
554     this->isMouseOver_ = false;
555 
556     this->overlay_->hide();
557 
558     TooltipWidget::instance()->hide();
559 
560     this->handleModifiers(QGuiApplication::queryKeyboardModifiers());
561 }
562 
focusInEvent(QFocusEvent * event)563 void Split::focusInEvent(QFocusEvent *event)
564 {
565     this->giveFocus(event->reason());
566 }
567 
handleModifiers(Qt::KeyboardModifiers modifiers)568 void Split::handleModifiers(Qt::KeyboardModifiers modifiers)
569 {
570     if (modifierStatus != modifiers)
571     {
572         modifierStatus = modifiers;
573         modifierStatusChanged.invoke(modifiers);
574     }
575 }
576 
setIsTopRightSplit(bool value)577 void Split::setIsTopRightSplit(bool value)
578 {
579     this->isTopRightSplit_ = value;
580     this->header_->setAddButtonVisible(value);
581 }
582 
583 /// Slots
addSibling()584 void Split::addSibling()
585 {
586     this->actionRequested.invoke(Action::AppendNewSplit);
587 }
588 
deleteFromContainer()589 void Split::deleteFromContainer()
590 {
591     this->actionRequested.invoke(Action::Delete);
592 }
593 
changeChannel()594 void Split::changeChannel()
595 {
596     this->showChangeChannelPopup("Change channel", false, [](bool) {});
597 
598     auto popup = this->findChildren<QDockWidget *>();
599     if (popup.size() && popup.at(0)->isVisible() && !popup.at(0)->isFloating())
600     {
601         popup.at(0)->hide();
602         showViewerList();
603     }
604 }
605 
explainMoving()606 void Split::explainMoving()
607 {
608     showTutorialVideo(this, ":/examples/moving.gif", "Moving",
609                       "Hold <Ctrl+Alt> to move splits.\n\nExample:");
610 }
611 
explainSplitting()612 void Split::explainSplitting()
613 {
614     showTutorialVideo(this, ":/examples/splitting.gif", "Splitting",
615                       "Hold <Ctrl+Alt> to add new splits.\n\nExample:");
616 }
617 
popup()618 void Split::popup()
619 {
620     auto app = getApp();
621     Window &window = app->windows->createWindow(WindowType::Popup);
622 
623     Split *split = new Split(static_cast<SplitContainer *>(
624         window.getNotebook().getOrAddSelectedPage()));
625 
626     split->setChannel(this->getIndirectChannel());
627     window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
628 
629     window.show();
630 }
631 
clear()632 void Split::clear()
633 {
634     this->view_->clearMessages();
635 }
636 
openInBrowser()637 void Split::openInBrowser()
638 {
639     auto channel = this->getChannel();
640 
641     if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
642     {
643         QDesktopServices::openUrl("https://twitch.tv/" +
644                                   twitchChannel->getName());
645     }
646 }
647 
openWhispersInBrowser()648 void Split::openWhispersInBrowser()
649 {
650     auto userName = getApp()->accounts->twitch.getCurrent()->getUserName();
651     QDesktopServices::openUrl("https://twitch.tv/popout/moderator/" + userName +
652                               "/whispers");
653 }
654 
openBrowserPlayer()655 void Split::openBrowserPlayer()
656 {
657     this->openChannelInBrowserPlayer(this->getChannel());
658 }
659 
openModViewInBrowser()660 void Split::openModViewInBrowser()
661 {
662     auto channel = this->getChannel();
663 
664     if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
665     {
666         QDesktopServices::openUrl("https://twitch.tv/moderator/" +
667                                   twitchChannel->getName());
668     }
669 }
670 
openInStreamlink()671 void Split::openInStreamlink()
672 {
673     this->openChannelInStreamlink(this->getChannel()->getName());
674 }
675 
openWithCustomScheme()676 void Split::openWithCustomScheme()
677 {
678     QString scheme = getSettings()->customURIScheme.getValue();
679     if (scheme.isEmpty())
680     {
681         return;
682     }
683 
684     const auto channel = this->getChannel().get();
685 
686     if (const auto twitchChannel = dynamic_cast<TwitchChannel *>(channel))
687     {
688         QDesktopServices::openUrl(QString("%1https://twitch.tv/%2")
689                                       .arg(scheme)
690                                       .arg(twitchChannel->getName()));
691     }
692 }
693 
showViewerList()694 void Split::showViewerList()
695 {
696     auto viewerDock =
697         new QDockWidget("Viewer List - " + this->getChannel()->getName(), this);
698     viewerDock->setAllowedAreas(Qt::LeftDockWidgetArea);
699     viewerDock->setFeatures(QDockWidget::DockWidgetVerticalTitleBar |
700                             QDockWidget::DockWidgetClosable |
701                             QDockWidget::DockWidgetFloatable);
702     viewerDock->resize(
703         0.5 * this->width(),
704         this->height() - this->header_->height() - this->input_->height());
705     viewerDock->move(0, this->header_->height());
706 
707     auto multiWidget = new QWidget(viewerDock);
708     auto dockVbox = new QVBoxLayout(viewerDock);
709     auto searchBar = new QLineEdit(viewerDock);
710 
711     auto chattersList = new QListWidget();
712     auto resultList = new QListWidget();
713 
714     auto formatListItemText = [](QString text) {
715         auto item = new QListWidgetItem();
716         item->setText(text);
717         item->setFont(getApp()->fonts->getFont(FontStyle::ChatMedium, 1.0));
718         return item;
719     };
720 
721     static QStringList labels = {
722         "Broadcaster", "Moderators",        "VIPs",   "Staff",
723         "Admins",      "Global Moderators", "Viewers"};
724     static QStringList jsonLabels = {"broadcaster", "moderators", "vips",
725                                      "staff",       "admins",     "global_mods",
726                                      "viewers"};
727     QList<QListWidgetItem *> labelList;
728     for (auto &x : labels)
729     {
730         auto label = formatListItemText(x);
731         label->setForeground(this->theme->accent);
732         labelList.append(label);
733     }
734     auto loadingLabel = new QLabel("Loading...");
735 
736     NetworkRequest::twitchRequest("https://tmi.twitch.tv/group/user/" +
737                                   this->getChannel()->getName() + "/chatters")
738         .caller(this)
739         .onSuccess([=](auto result) -> Outcome {
740             auto obj = result.parseJson();
741             QJsonObject chattersObj = obj.value("chatters").toObject();
742 
743             loadingLabel->hide();
744             for (int i = 0; i < jsonLabels.size(); i++)
745             {
746                 auto currentCategory =
747                     chattersObj.value(jsonLabels.at(i)).toArray();
748                 // If current category of chatters is empty, dont show this
749                 // category.
750                 if (currentCategory.empty())
751                     continue;
752 
753                 chattersList->addItem(labelList.at(i));
754                 foreach (const QJsonValue &v, currentCategory)
755                 {
756                     chattersList->addItem(formatListItemText(v.toString()));
757                 }
758                 chattersList->addItem(new QListWidgetItem());
759             }
760 
761             return Success;
762         })
763         .execute();
764 
765     searchBar->setPlaceholderText("Search User...");
766     QObject::connect(searchBar, &QLineEdit::textEdited, this, [=]() {
767         auto query = searchBar->text();
768         if (!query.isEmpty())
769         {
770             auto results = chattersList->findItems(query, Qt::MatchContains);
771             chattersList->hide();
772             resultList->clear();
773             for (auto &item : results)
774             {
775                 if (!labels.contains(item->text()))
776                 {
777                     resultList->addItem(formatListItemText(item->text()));
778                 }
779             }
780             resultList->show();
781         }
782         else
783         {
784             resultList->hide();
785             chattersList->show();
786         }
787     });
788 
789     QObject::connect(viewerDock, &QDockWidget::topLevelChanged, this, [=]() {
790         viewerDock->setMinimumWidth(300);
791     });
792 
793     auto listDoubleClick = [=](QString userName) {
794         if (!labels.contains(userName) && !userName.isEmpty())
795         {
796             this->view_->showUserInfoPopup(userName);
797         }
798     };
799 
800     QObject::connect(chattersList, &QListWidget::doubleClicked, this, [=]() {
801         listDoubleClick(chattersList->currentItem()->text());
802     });
803 
804     QObject::connect(resultList, &QListWidget::doubleClicked, this, [=]() {
805         listDoubleClick(resultList->currentItem()->text());
806     });
807 
808     dockVbox->addWidget(searchBar);
809     dockVbox->addWidget(loadingLabel);
810     dockVbox->addWidget(chattersList);
811     dockVbox->addWidget(resultList);
812     resultList->hide();
813 
814     multiWidget->setStyleSheet(this->theme->splits.input.styleSheet);
815     multiWidget->setLayout(dockVbox);
816     viewerDock->setWidget(multiWidget);
817     viewerDock->setFloating(true);
818     viewerDock->show();
819     viewerDock->activateWindow();
820 }
821 
openSubPage()822 void Split::openSubPage()
823 {
824     ChannelPtr channel = this->getChannel();
825 
826     if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
827     {
828         QDesktopServices::openUrl(twitchChannel->subscriptionUrl());
829     }
830 }
831 
copyToClipboard()832 void Split::copyToClipboard()
833 {
834     crossPlatformCopy(this->view_->getSelectedText());
835 }
836 
setFiltersDialog()837 void Split::setFiltersDialog()
838 {
839     SelectChannelFiltersDialog d(this->getFilters(), this);
840     d.setWindowTitle("Select filters");
841 
842     if (d.exec() == QDialog::Accepted)
843     {
844         this->setFilters(d.getSelection());
845     }
846 }
847 
setFilters(const QList<QUuid> ids)848 void Split::setFilters(const QList<QUuid> ids)
849 {
850     this->view_->setFilters(ids);
851     this->header_->updateChannelText();
852 }
853 
getFilters() const854 const QList<QUuid> Split::getFilters() const
855 {
856     return this->view_->getFilterIds();
857 }
858 
showSearch()859 void Split::showSearch()
860 {
861     SearchPopup *popup = new SearchPopup(this);
862 
863     popup->setChannelFilters(this->view_->getFilterSet());
864     popup->setAttribute(Qt::WA_DeleteOnClose);
865     popup->setChannel(this->getChannel());
866     popup->show();
867 }
868 
reloadChannelAndSubscriberEmotes()869 void Split::reloadChannelAndSubscriberEmotes()
870 {
871     getApp()->accounts->twitch.getCurrent()->loadEmotes();
872     auto channel = this->getChannel();
873 
874     if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
875     {
876         twitchChannel->refreshBTTVChannelEmotes(true);
877         twitchChannel->refreshFFZChannelEmotes(true);
878     }
879 }
880 
reconnect()881 void Split::reconnect()
882 {
883     this->getChannel()->reconnect();
884 }
885 
dragEnterEvent(QDragEnterEvent * event)886 void Split::dragEnterEvent(QDragEnterEvent *event)
887 {
888     if (getSettings()->imageUploaderEnabled &&
889         (event->mimeData()->hasImage() || event->mimeData()->hasUrls()))
890     {
891         event->acceptProposedAction();
892     }
893     else
894     {
895         BaseWidget::dragEnterEvent(event);
896     }
897 }
898 
dropEvent(QDropEvent * event)899 void Split::dropEvent(QDropEvent *event)
900 {
901     if (getSettings()->imageUploaderEnabled &&
902         (event->mimeData()->hasImage() || event->mimeData()->hasUrls()))
903     {
904         this->input_->ui_.textEdit->imagePasted.invoke(event->mimeData());
905     }
906     else
907     {
908         BaseWidget::dropEvent(event);
909     }
910 }
911 template <typename Iter, typename RandomGenerator>
select_randomly(Iter start,Iter end,RandomGenerator & g)912 static Iter select_randomly(Iter start, Iter end, RandomGenerator &g)
913 {
914     std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1);
915     std::advance(start, dis(g));
916     return start;
917 }
918 
919 template <typename Iter>
select_randomly(Iter start,Iter end)920 static Iter select_randomly(Iter start, Iter end)
921 {
922     static std::random_device rd;
923     static std::mt19937 gen(rd());
924     return select_randomly(start, end, gen);
925 }
926 
drag()927 void Split::drag()
928 {
929     if (auto container = dynamic_cast<SplitContainer *>(this->parentWidget()))
930     {
931         SplitContainer::isDraggingSplit = true;
932         SplitContainer::draggingSplit = this;
933 
934         auto originalLocation = container->releaseSplit(this);
935         auto drag = new QDrag(this);
936         auto mimeData = new QMimeData;
937 
938         mimeData->setData("chatterino/split", "xD");
939         drag->setMimeData(mimeData);
940 
941         if (drag->exec(Qt::MoveAction) == Qt::IgnoreAction)
942         {
943             container->insertSplit(this, originalLocation);
944         }
945 
946         SplitContainer::isDraggingSplit = false;
947     }
948 }
949 
950 }  // namespace chatterino
951