1 #include "SearchPopup.hpp"
2 
3 #include <QHBoxLayout>
4 #include <QLineEdit>
5 #include <QPushButton>
6 #include <QVBoxLayout>
7 
8 #include "common/Channel.hpp"
9 #include "messages/Message.hpp"
10 #include "messages/search/AuthorPredicate.hpp"
11 #include "messages/search/ChannelPredicate.hpp"
12 #include "messages/search/LinkPredicate.hpp"
13 #include "messages/search/MessageFlagsPredicate.hpp"
14 #include "messages/search/SubstringPredicate.hpp"
15 #include "util/Shortcut.hpp"
16 #include "widgets/helper/ChannelView.hpp"
17 
18 namespace chatterino {
19 
filter(const QString & text,const QString & channelName,const LimitedQueueSnapshot<MessagePtr> & snapshot,FilterSetPtr filterSet)20 ChannelPtr SearchPopup::filter(const QString &text, const QString &channelName,
21                                const LimitedQueueSnapshot<MessagePtr> &snapshot,
22                                FilterSetPtr filterSet)
23 {
24     ChannelPtr channel(new Channel(channelName, Channel::Type::None));
25 
26     // Parse predicates from tags in "text"
27     auto predicates = parsePredicates(text);
28 
29     // Check for every message whether it fulfills all predicates that have
30     // been registered
31     for (size_t i = 0; i < snapshot.size(); ++i)
32     {
33         MessagePtr message = snapshot[i];
34 
35         bool accept = true;
36         for (const auto &pred : predicates)
37         {
38             // Discard the message as soon as one predicate fails
39             if (!pred->appliesTo(*message))
40             {
41                 accept = false;
42                 break;
43             }
44         }
45 
46         if (accept && filterSet)
47             accept = filterSet->filter(message, channel);
48 
49         // If all predicates match, add the message to the channel
50         if (accept)
51             channel->addMessage(message);
52     }
53 
54     return channel;
55 }
56 
SearchPopup(QWidget * parent)57 SearchPopup::SearchPopup(QWidget *parent)
58     : BasePopup({}, parent)
59 {
60     this->initLayout();
61     this->resize(400, 600);
62 
__anon381732700102null63     createShortcut(this, "CTRL+F", [this] {
64         this->searchInput_->setFocus();
65         this->searchInput_->selectAll();
66     });
67 }
68 
setChannelFilters(FilterSetPtr filters)69 void SearchPopup::setChannelFilters(FilterSetPtr filters)
70 {
71     this->channelFilters_ = std::move(filters);
72 }
73 
setChannel(const ChannelPtr & channel)74 void SearchPopup::setChannel(const ChannelPtr &channel)
75 {
76     this->channelView_->setSourceChannel(channel);
77     this->channelName_ = channel->getName();
78     this->snapshot_ = channel->getMessageSnapshot();
79     this->search();
80 
81     this->updateWindowTitle();
82 }
83 
updateWindowTitle()84 void SearchPopup::updateWindowTitle()
85 {
86     QString historyName;
87 
88     if (this->channelName_ == "/whispers")
89     {
90         historyName = "whispers";
91     }
92     else if (this->channelName_ == "/mentions")
93     {
94         historyName = "mentions";
95     }
96     else if (this->channelName_.isEmpty())
97     {
98         historyName = "<empty>'s";
99     }
100     else
101     {
102         historyName = QString("%1's").arg(this->channelName_);
103     }
104     this->setWindowTitle("Searching in " + historyName + " history");
105 }
106 
search()107 void SearchPopup::search()
108 {
109     this->channelView_->setChannel(filter(this->searchInput_->text(),
110                                           this->channelName_, this->snapshot_,
111                                           this->channelFilters_));
112 }
113 
initLayout()114 void SearchPopup::initLayout()
115 {
116     // VBOX
117     {
118         QVBoxLayout *layout1 = new QVBoxLayout(this);
119         layout1->setMargin(0);
120         layout1->setSpacing(0);
121 
122         // HBOX
123         {
124             QHBoxLayout *layout2 = new QHBoxLayout(this);
125             layout2->setMargin(8);
126             layout2->setSpacing(8);
127 
128             // SEARCH INPUT
129             {
130                 this->searchInput_ = new QLineEdit(this);
131                 layout2->addWidget(this->searchInput_);
132                 QObject::connect(this->searchInput_, &QLineEdit::returnPressed,
133                                  [this] {
134                                      this->search();
135                                  });
136             }
137 
138             // SEARCH BUTTON
139             {
140                 QPushButton *searchButton = new QPushButton(this);
141                 searchButton->setText("Search");
142                 layout2->addWidget(searchButton);
143                 QObject::connect(searchButton, &QPushButton::clicked, [this] {
144                     this->search();
145                 });
146             }
147 
148             layout1->addLayout(layout2);
149         }
150 
151         // CHANNELVIEW
152         {
153             this->channelView_ = new ChannelView(this);
154 
155             layout1->addWidget(this->channelView_);
156         }
157 
158         this->setLayout(layout1);
159     }
160 
161     this->searchInput_->setFocus();
162 }
163 
parsePredicates(const QString & input)164 std::vector<std::unique_ptr<MessagePredicate>> SearchPopup::parsePredicates(
165     const QString &input)
166 {
167     static QRegularExpression predicateRegex(R"(^(\w+):([\w,]+)$)");
168 
169     std::vector<std::unique_ptr<MessagePredicate>> predicates;
170     auto words = input.split(' ', QString::SkipEmptyParts);
171     QStringList authors;
172     QStringList channels;
173 
174     for (auto it = words.begin(); it != words.end();)
175     {
176         if (auto match = predicateRegex.match(*it); match.hasMatch())
177         {
178             QString name = match.captured(1);
179             QString value = match.captured(2);
180 
181             bool remove = true;
182 
183             // match predicates
184             if (name == "from")
185             {
186                 authors.append(value);
187             }
188             else if (name == "has" && value == "link")
189             {
190                 predicates.push_back(std::make_unique<LinkPredicate>());
191             }
192             else if (name == "in")
193             {
194                 channels.append(value);
195             }
196             else if (name == "is")
197             {
198                 predicates.push_back(
199                     std::make_unique<MessageFlagsPredicate>(value));
200             }
201             else
202             {
203                 remove = false;
204             }
205 
206             // remove or advance
207             it = remove ? words.erase(it) : ++it;
208         }
209         else
210         {
211             ++it;
212         }
213     }
214 
215     if (!authors.empty())
216         predicates.push_back(std::make_unique<AuthorPredicate>(authors));
217 
218     if (!channels.empty())
219         predicates.push_back(std::make_unique<ChannelPredicate>(channels));
220 
221     if (!words.empty())
222         predicates.push_back(
223             std::make_unique<SubstringPredicate>(words.join(" ")));
224 
225     return predicates;
226 }
227 
228 }  // namespace chatterino
229