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