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