1 #include "common/CompletionModel.hpp"
2
3 #include "Application.hpp"
4 #include "common/ChatterSet.hpp"
5 #include "common/Common.hpp"
6 #include "controllers/accounts/AccountController.hpp"
7 #include "controllers/commands/CommandController.hpp"
8 #include "debug/Benchmark.hpp"
9 #include "providers/twitch/TwitchChannel.hpp"
10 #include "providers/twitch/TwitchIrcServer.hpp"
11 #include "singletons/Emotes.hpp"
12 #include "singletons/Settings.hpp"
13 #include "util/Helpers.hpp"
14 #include "util/QStringHash.hpp"
15
16 #include <QtAlgorithms>
17 #include <utility>
18
19 namespace chatterino {
20
21 //
22 // TaggedString
23 //
24
TaggedString(const QString & _string,Type _type)25 CompletionModel::TaggedString::TaggedString(const QString &_string, Type _type)
26 : string(_string)
27 , type(_type)
28 {
29 }
30
isEmote() const31 bool CompletionModel::TaggedString::isEmote() const
32 {
33 return this->type > Type::EmoteStart && this->type < Type::EmoteEnd;
34 }
35
operator <(const TaggedString & that) const36 bool CompletionModel::TaggedString::operator<(const TaggedString &that) const
37 {
38 if (this->isEmote() != that.isEmote())
39 {
40 return this->isEmote();
41 }
42
43 return CompletionModel::compareStrings(this->string, that.string);
44 }
45
46 //
47 // CompletionModel
48 //
CompletionModel(Channel & channel)49 CompletionModel::CompletionModel(Channel &channel)
50 : channel_(channel)
51 {
52 }
53
columnCount(const QModelIndex &) const54 int CompletionModel::columnCount(const QModelIndex &) const
55 {
56 return 1;
57 }
58
data(const QModelIndex & index,int) const59 QVariant CompletionModel::data(const QModelIndex &index, int) const
60 {
61 std::lock_guard<std::mutex> lock(this->itemsMutex_);
62
63 auto it = this->items_.begin();
64 std::advance(it, index.row());
65 return QVariant(it->string);
66 }
67
rowCount(const QModelIndex &) const68 int CompletionModel::rowCount(const QModelIndex &) const
69 {
70 std::lock_guard<std::mutex> lock(this->itemsMutex_);
71
72 return this->items_.size();
73 }
74
refresh(const QString & prefix,bool isFirstWord)75 void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
76 {
77 std::lock_guard<std::mutex> guard(this->itemsMutex_);
78 this->items_.clear();
79
80 if (prefix.length() < 2 || !this->channel_.isTwitchChannel())
81 {
82 return;
83 }
84
85 // Twitch channel
86 auto tc = dynamic_cast<TwitchChannel *>(&this->channel_);
87
88 std::function<void(const QString &str, TaggedString::Type type)> addString;
89 if (getSettings()->prefixOnlyEmoteCompletion)
90 {
91 addString = [=](const QString &str, TaggedString::Type type) {
92 if (str.startsWith(prefix, Qt::CaseInsensitive))
93 this->items_.emplace(str + " ", type);
94 };
95 }
96 else
97 {
98 addString = [=](const QString &str, TaggedString::Type type) {
99 if (str.contains(prefix, Qt::CaseInsensitive))
100 this->items_.emplace(str + " ", type);
101 };
102 }
103
104 if (auto account = getApp()->accounts->twitch.getCurrent())
105 {
106 // Twitch Emotes available globally
107 for (const auto &emote : account->accessEmotes()->emotes)
108 {
109 addString(emote.first.string, TaggedString::TwitchGlobalEmote);
110 }
111
112 // Twitch Emotes available locally
113 auto localEmoteData = account->accessLocalEmotes();
114 if (tc && localEmoteData->find(tc->roomId()) != localEmoteData->end())
115 {
116 for (const auto &emote : localEmoteData->at(tc->roomId()))
117 {
118 addString(emote.first.string,
119 TaggedString::Type::TwitchLocalEmote);
120 }
121 }
122 }
123
124 // Bttv Global
125 for (auto &emote : *getApp()->twitch2->getBttvEmotes().emotes())
126 {
127 addString(emote.first.string, TaggedString::Type::BTTVChannelEmote);
128 }
129
130 // Ffz Global
131 for (auto &emote : *getApp()->twitch2->getFfzEmotes().emotes())
132 {
133 addString(emote.first.string, TaggedString::Type::FFZChannelEmote);
134 }
135
136 // Emojis
137 if (prefix.startsWith(":"))
138 {
139 const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes;
140 for (auto &m : emojiShortCodes)
141 {
142 addString(QString(":%1:").arg(m), TaggedString::Type::Emoji);
143 }
144 }
145
146 //
147 // Stuff below is available only in regular Twitch channels
148 if (!tc)
149 {
150 return;
151 }
152
153 // Usernames
154 if (prefix.startsWith("@"))
155 {
156 QString usernamePrefix = prefix;
157 usernamePrefix.remove(0, 1);
158
159 auto chatters = tc->accessChatters()->filterByPrefix(usernamePrefix);
160
161 for (const auto &name : chatters)
162 {
163 addString(
164 "@" + formatUserMention(name, isFirstWord,
165 getSettings()->mentionUsersWithComma),
166 TaggedString::Type::Username);
167 }
168 }
169 else if (!getSettings()->userCompletionOnlyWithAt)
170 {
171 auto chatters = tc->accessChatters()->filterByPrefix(prefix);
172
173 for (const auto &name : chatters)
174 {
175 addString(formatUserMention(name, isFirstWord,
176 getSettings()->mentionUsersWithComma),
177 TaggedString::Type::Username);
178 }
179 }
180
181 // Bttv Channel
182 for (auto &emote : *tc->bttvEmotes())
183 {
184 addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
185 }
186
187 // Ffz Channel
188 for (auto &emote : *tc->ffzEmotes())
189 {
190 addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
191 }
192
193 // Commands
194 for (auto &command : getApp()->commands->items_)
195 {
196 addString(command.name, TaggedString::Command);
197 }
198
199 for (auto &command : getApp()->commands->getDefaultTwitchCommandList())
200 {
201 addString(command, TaggedString::Command);
202 }
203 }
204
compareStrings(const QString & a,const QString & b)205 bool CompletionModel::compareStrings(const QString &a, const QString &b)
206 {
207 // try comparing insensitively, if they are the same then senstively
208 // (fixes order of LuL and LUL)
209 int k = QString::compare(a, b, Qt::CaseInsensitive);
210 if (k == 0)
211 return a > b;
212
213 return k < 0;
214 }
215
216 } // namespace chatterino
217