Hello community,

here is the log from the commit of package orion for openSUSE:Factory checked 
in at 2017-06-04 02:01:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/orion (Old)
 and      /work/SRC/openSUSE:Factory/.orion.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "orion"

Sun Jun  4 02:01:28 2017 rev:6 rq:500859 version:1.5.1+git~20170602

Changes:
--------
--- /work/SRC/openSUSE:Factory/orion/orion.changes      2017-05-31 
12:19:23.295398204 +0200
+++ /work/SRC/openSUSE:Factory/.orion.new/orion.changes 2017-06-04 
02:01:37.648969159 +0200
@@ -1,0 +2,15 @@
+Fri Jun 02 09:05:56 UTC 2017 - [email protected]
+
+- Update to version 1.5.1+git~20170602:
+  * collect _total from responses to be shown in search view; stop cascading 
requests from responses once the total from the previous response is reached 
(#171)
+  * BTTV Emote Support (#168)
+  * when watching a VOD with chat replay, show a more consistent number of 
previous messages after seeking (#169)
+
+-------------------------------------------------------------------
+Thu Jun 01 16:03:31 UTC 2017 - [email protected]
+
+- Update to version 1.5.1+git~20170601:
+  * switch followed and blocked paged results handling to use JSON _total and 
not stop immediately after a non-full page (#166)
+- Include fontawesome-fonts as dependency to fix GUI issues
+
+-------------------------------------------------------------------

Old:
----
  orion-1.5.1+git~20170519.tar.xz

New:
----
  orion-1.5.1+git~20170602.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ orion.spec ++++++
--- /var/tmp/diff_new_pack.tK3Gdm/_old  2017-06-04 02:01:39.580696498 +0200
+++ /var/tmp/diff_new_pack.tK3Gdm/_new  2017-06-04 02:01:39.580696498 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           orion
-Version:        1.5.1+git~20170519
+Version:        1.5.1+git~20170602
 Release:        0
 Summary:        Twitch stream client using Qt
 License:        GPL-3.0
@@ -33,9 +33,12 @@
 BuildRequires:  pkgconfig(Qt5Svg)           >=  5.6
 BuildRequires:  pkgconfig(mpv)
 
-#Required for GUI to function, It is not added automatically by OBS (probably 
due to QML?)
+#Required for GUI to function, it is not added automatically by OBS (probably 
due to QML?)
 Requires:       libqt5-qtquickcontrols2
 
+#Required to display GUI icons properly
+Requires:       fontawesome-fonts
+
 Requires(post):     hicolor-icon-theme
 Requires(postun):   hicolor-icon-theme
 Requires(post):     update-desktop-files

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.tK3Gdm/_old  2017-06-04 02:01:39.624690288 +0200
+++ /var/tmp/diff_new_pack.tK3Gdm/_new  2017-06-04 02:01:39.624690288 +0200
@@ -1,4 +1,4 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/alamminsalo/orion.git</param>
-              <param 
name="changesrevision">0f79a6bb5132c97ba1602b59596eeb669a4977f0</param></service></servicedata>
\ No newline at end of file
+              <param 
name="changesrevision">fe4787b43bf32c4795031965fade34e1f797f7f2</param></service></servicedata>
\ No newline at end of file

++++++ orion-1.5.1+git~20170519.tar.xz -> orion-1.5.1+git~20170602.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/orion-1.5.1+git~20170519/src/model/channelmanager.cpp 
new/orion-1.5.1+git~20170602/src/model/channelmanager.cpp
--- old/orion-1.5.1+git~20170519/src/model/channelmanager.cpp   2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/channelmanager.cpp   2017-06-02 
07:42:07.000000000 +0200
@@ -188,6 +188,9 @@
     connect(netman, &NetworkManager::getChannelBitsUrlsOperationFinished, 
this, &ChannelManager::innerChannelBitsDataLoaded);
     connect(netman, &NetworkManager::getGlobalBitsUrlsOperationFinished, this, 
&ChannelManager::innerGlobalBitsDataLoaded);
 
+    connect(netman, &NetworkManager::getGlobalBttvEmotesOperationFinished, 
this, &ChannelManager::innerGlobalBttvEmotesLoaded);
+    connect(netman, &NetworkManager::getChannelBttvEmotesOperationFinished, 
this, &ChannelManager::innerChannelBttvEmotesLoaded);
+
     connect(netman, &NetworkManager::favouritesReplyFinished, this, 
&ChannelManager::addFollowedResults);
     connect(netman, &NetworkManager::vodStartGetOperationFinished, this, 
&ChannelManager::vodStartGetOperationFinished);
     connect(netman, &NetworkManager::vodChatPieceGetOperationFinished, this, 
&ChannelManager::vodChatPieceGetOperationFinished);
@@ -581,7 +584,7 @@
     emit searchingStarted();
 }
 
-void ChannelManager::addSearchResults(const QList<Channel*> &list)
+void ChannelManager::addSearchResults(const QList<Channel*> &list, const int 
total)
 {
     bool needsStreamCheck = false;
 
@@ -600,7 +603,7 @@
 
     qDeleteAll(list);
 
-    emit resultsUpdated(numAdded);
+    emit resultsUpdated(numAdded, total);
 }
 
 void ChannelManager::getFeatured()
@@ -818,6 +821,32 @@
     return out;
 }
 
+bool ChannelManager::loadChannelBttvEmotes(const QString channel) {
+    bool out = false;
+
+    auto result = channelBttvEmotes.constFind(channel);
+    if (result != channelBttvEmotes.constEnd()) {
+        // deliver cached channel bttv emotes
+        emit channelBttvEmotesLoaded(channel, result.value());
+    }
+    else {
+        netman->getChannelBttvEmotes(channel);
+        out = true;
+    }
+
+    const QString GLOBAL_EMOTES_IDENTIFIER = "GLOBAL";
+    result = channelBttvEmotes.constFind(GLOBAL_EMOTES_IDENTIFIER);
+    if (result != channelBttvEmotes.constEnd()) {
+        emit channelBttvEmotesLoaded(GLOBAL_EMOTES_IDENTIFIER, result.value());
+    }
+    else {
+        netman->getGlobalBttvEmotes();
+        out = true;
+    }
+
+    return out;
+}
+
 /*
 QVariantMap convertBitsUrls(const QMap<QString, QMap<QString, QString>> & 
bitsUrls) {
     QVariantMap out;
@@ -944,6 +973,19 @@
     emit channelBitsUrlsLoaded(GLOBAL_BITS_KEY, globalBitsUrls, 
globalBitsColors);
 }
 
+void ChannelManager::innerChannelBttvEmotesLoaded(const QString channel, 
QMap<QString, QString> & emotesByCode) {
+    channelBttvEmotes.remove(channel);
+    channelBttvEmotes.insert(channel, emotesByCode);
+    emit channelBttvEmotesLoaded(channel, emotesByCode);
+}
+
+void ChannelManager::innerGlobalBttvEmotesLoaded(QMap<QString, QString> & 
emotesByCode) {
+    const QString GLOBAL_EMOTES_KEY = "GLOBAL";
+    channelBttvEmotes.remove(GLOBAL_EMOTES_KEY);
+    channelBttvEmotes.insert(GLOBAL_EMOTES_KEY, emotesByCode);
+    emit channelBttvEmotesLoaded(GLOBAL_EMOTES_KEY, emotesByCode);
+}
+
 void ChannelManager::getFollowedChannels(const quint32& limit, const quint32& 
offset)
 {
     //if (offset == 0)
@@ -953,14 +995,14 @@
 }
 
 
-void ChannelManager::addFollowedResults(const QList<Channel *> &list, const 
quint32 offset)
+void ChannelManager::addFollowedResults(const QList<Channel *> &list, const 
quint32 offset, const quint32 total)
 {
     //    qDebug() << "Merging channel data for " << list.size()
     //             << " items with " << offset << " offset.";
 
     favouritesModel->mergeAll(list);
 
-    if (list.size() == FOLLOWED_FETCH_LIMIT)
+    if (offset < total)
         getFollowedChannels(FOLLOWED_FETCH_LIMIT, offset);
 
     checkStreams(list);
@@ -978,13 +1020,13 @@
     netman->getBlockedUserList(accessToken(), user_id, 0, 
BLOCKED_USER_LIST_FETCH_LIMIT);
 }
 
-void ChannelManager::addBlockedUserResults(const QList<QString> & list, const 
quint32 nextOffset)
+void ChannelManager::addBlockedUserResults(const QList<QString> & list, const 
quint32 nextOffset, const quint32 total)
 {
     if (!user_id || accessToken().isEmpty()) return;
 
     blockedUserListLoading.append(list);
 
-    if (list.size() == BLOCKED_USER_LIST_FETCH_LIMIT) {
+    if (nextOffset < total) {
         netman->getBlockedUserList(accessToken(), user_id, nextOffset, 
BLOCKED_USER_LIST_FETCH_LIMIT);
     }
     else {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/model/channelmanager.h 
new/orion-1.5.1+git~20170602/src/model/channelmanager.h
--- old/orion-1.5.1+git~20170519/src/model/channelmanager.h     2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/channelmanager.h     2017-06-02 
07:42:07.000000000 +0200
@@ -114,6 +114,7 @@
     QMap<int, QMap<int, QString>> lastEmoteSets;
     QMap<QString, QMap<QString, QMap<QString, QString>>> channelBadgeUrls;
     QMap<QString, QMap<QString, QMap<QString, QMap<QString, QString>>>> 
channelBadgeBetaUrls;
+    QMap<QString, QMap<QString, QString>> channelBttvEmotes;
     QMap<int, QMap<QString, QMap<QString, QString>>> channelBitsUrls;
     QMap<int, QMap<QString, QMap<QString, QString>>> channelBitsColors;
 
@@ -179,6 +180,8 @@
     Q_INVOKABLE bool loadChannelBitsUrls(int channel);
     Q_INVOKABLE void loadChatterList(const QString channel);
 
+    Q_INVOKABLE bool loadChannelBttvEmotes(const QString channel);
+
     Q_INVOKABLE void cancelLastVodChatRequest();
     Q_INVOKABLE void resetVodChat();
     Q_INVOKABLE void getVodStartTime(quint64 vodId);
@@ -269,7 +272,7 @@
 
 signals:
     void pushNotification(const QString &title, const QString &message, const 
QString &imgUrl);
-    void resultsUpdated(int numAdded);
+    void resultsUpdated(int numAdded, int total);
     void featuredUpdated();
     void searchingStarted();
     void foundPlaybackStream(const QVariantMap &streams);
@@ -292,6 +295,8 @@
 
     void channelBitsUrlsLoaded(const int channelID, BitsQStringsMap bitsUrls, 
BitsQStringsMap bitsColors);
 
+    void channelBttvEmotesLoaded(const QString channel, QMap<QString, QString> 
emotesByCode);
+
     void vodStartGetOperationFinished(double);
     void vodChatPieceGetOperationFinished(QList<ReplayChatMessage>);
 
@@ -316,7 +321,7 @@
     void setAccessToken(const QString &arg);
 
 private slots:
-    void addSearchResults(const QList<Channel*>&);
+    void addSearchResults(const QList<Channel*>&, const int total);
     void addFeaturedResults(const QList<Channel*>&);
     void updateFavourites(const QList<Channel*>&);
     void updateStreams(const QList<Channel*>&);
@@ -328,10 +333,12 @@
     void innerGlobalBadgeBetaUrlsLoaded(const QMap<QString, QMap<QString, 
QMap<QString, QString>>> badgeData);
     void innerChannelBitsDataLoaded(int channelID, BitsQStringsMap 
channelBitsUrls, BitsQStringsMap channelBitsColors);
     void innerGlobalBitsDataLoaded(BitsQStringsMap globalBitsUrls, 
BitsQStringsMap globalBitsColors);
-    void addFollowedResults(const QList<Channel*>&, const quint32);
+    void innerChannelBttvEmotesLoaded(const QString channel, QMap<QString, 
QString> & emotesByCode);
+    void innerGlobalBttvEmotesLoaded(QMap<QString, QString> & emotesByCode);
+    void addFollowedResults(const QList<Channel*>&, const quint32, const 
quint32);
     void onNetworkAccessChanged(bool);
     void processChatterList(QMap<QString, QList<QString>> chatters);
-    void addBlockedUserResults(const QList<QString> & list, const quint32 
nextOffset);
+    void addBlockedUserResults(const QList<QString> & list, const quint32 
nextOffset, const quint32 total);
     void innerUserBlocked(quint64 myUserId, const QString & blockedUsername);
     void innerUserUnblocked(quint64 myUserId, const QString & 
unblockedUsername);
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/model/imageprovider.cpp 
new/orion-1.5.1+git~20170602/src/model/imageprovider.cpp
--- old/orion-1.5.1+git~20170519/src/model/imageprovider.cpp    2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/imageprovider.cpp    2017-06-02 
07:42:07.000000000 +0200
@@ -163,7 +163,7 @@
     return new CachedImageProvider(_imageTable);
 }
 
-bool ImageProvider::downloadsInProgress() {
+bool ImageProvider::downloadsInProgress() const {
     return activeDownloadCount > 0;
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/model/imageprovider.h 
new/orion-1.5.1+git~20170602/src/model/imageprovider.h
--- old/orion-1.5.1+git~20170519/src/model/imageprovider.h      2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/imageprovider.h      2017-06-02 
07:42:07.000000000 +0200
@@ -70,7 +70,7 @@
     * Returns true if caller should wait for a downloadComplete event before 
using the emote */
     bool makeAvailable(QString key);
     bool download(QString key);
-    bool downloadsInProgress();
+    bool downloadsInProgress() const;
 
     QHash<QString, QImage*> imageTable();
     void loadImageFile(QString key, QString filename);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/model/ircchat.cpp 
new/orion-1.5.1+git~20170602/src/model/ircchat.cpp
--- old/orion-1.5.1+git~20170519/src/model/ircchat.cpp  2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/ircchat.cpp  2017-06-02 
07:42:07.000000000 +0200
@@ -37,6 +37,9 @@
 const QString IrcChat::IMAGE_PROVIDER_BITS = "bits";
 const QString IrcChat::EMOTICONS_URL_FORMAT_LODPI = 
"https://static-cdn.jtvnw.net/emoticons/v1/%1/1.0";;
 const QString IrcChat::EMOTICONS_URL_FORMAT_HIDPI = 
"https://static-cdn.jtvnw.net/emoticons/v1/%1/2.0";;
+const QString IrcChat::IMAGE_PROVIDER_BTTV_EMOTE = "bttvemote";
+const QString IrcChat::BTTV_EMOTES_URL_FORMAT_LODPI = 
"https://cdn.betterttv.net/emote/%1/1x";;
+const QString IrcChat::BTTV_EMOTES_URL_FORMAT_HIDPI = 
"https://cdn.betterttv.net/emote/%1/2x";;
 
 const qint16 IrcChat::PORT = 443;
 const QString IrcChat::HOST = "irc.chat.twitch.tv";
@@ -55,6 +58,7 @@
 IrcChat::IrcChat(QObject *parent) :
     QObject(parent),
     _emoteProvider(IMAGE_PROVIDER_EMOTE, hiDpi? EMOTICONS_URL_FORMAT_HIDPI : 
EMOTICONS_URL_FORMAT_LODPI, ".png", hiDpi? "emotes_2x" : "emotes"),
+    _bttvEmoteProvider(IMAGE_PROVIDER_BTTV_EMOTE, hiDpi? 
BTTV_EMOTES_URL_FORMAT_HIDPI : BTTV_EMOTES_URL_FORMAT_LODPI, ".png", hiDpi? 
"bttv_emotes_2x" : "bttv_emotes"),
     _bitsProvider(nullptr),
     _badgeProvider(nullptr),
     _cman(nullptr),
@@ -64,8 +68,11 @@
 
     logged_in = false;
 
-    connect(&_emoteProvider, &ImageProvider::downloadComplete, this, 
&IrcChat::handleDownloadComplete);
-    connect(&_emoteProvider, &ImageProvider::bulkDownloadComplete, this, 
&IrcChat::bulkDownloadComplete);
+
+    for (const auto provider : { &_emoteProvider, &_bttvEmoteProvider }) {
+        connect(provider, &ImageProvider::downloadComplete, this, 
&IrcChat::handleDownloadComplete);
+        connect(provider, &ImageProvider::bulkDownloadComplete, this, 
&IrcChat::bulkDownloadComplete);
+    }
 
     room = "";
 
@@ -97,6 +104,7 @@
 
 void IrcChat::RegisterEngineProviders(QQmlEngine & engine) {
        engine.addImageProvider(IMAGE_PROVIDER_EMOTE, 
_emoteProvider.getQMLImageProvider());
+    engine.addImageProvider(IMAGE_PROVIDER_BTTV_EMOTE, 
_bttvEmoteProvider.getQMLImageProvider());
     if (_badgeProvider) {
         engine.addImageProvider(_badgeProvider->getImageProviderName(), 
_badgeProvider->getQMLImageProvider());
     }
@@ -118,6 +126,7 @@
         connect(_cman, &ChannelManager::vodStartGetOperationFinished, this, 
&IrcChat::handleVodStartTime);
         connect(_cman, &ChannelManager::vodChatPieceGetOperationFinished, 
this, &IrcChat::handleDownloadedReplayChat);
         connect(_cman, &ChannelManager::channelBitsUrlsLoaded, this, 
&IrcChat::handleChannelBitsUrlsLoaded);
+        connect(_cman, &ChannelManager::channelBttvEmotesLoaded, this, 
&IrcChat::handleChannelBttvEmotesLoaded);
         connect(_cman, &ChannelManager::blockedUsersLoaded, this, 
&IrcChat::blockedUsersLoaded);
         connect(_cman, &ChannelManager::userBlocked, this, 
&IrcChat::userBlocked);
         connect(_cman, &ChannelManager::userUnblocked, this, 
&IrcChat::userUnblocked);
@@ -127,6 +136,13 @@
     }
 }
 
+bool IrcChat::allDownloadsComplete() {
+    return !_emoteProvider.downloadsInProgress() &&
+        !_bttvEmoteProvider.downloadsInProgress() &&
+        _badgeProvider != nullptr && !_badgeProvider->downloadsInProgress() &&
+        _bitsProvider != nullptr && !_bitsProvider->downloadsInProgress();
+}
+
 IrcChat::~IrcChat() {
     disconnect();
 }
@@ -149,6 +165,8 @@
     if (_bitsProvider) {
         _bitsProvider->setChannelId(channelId.toInt());
     }
+
+    lastCurChannelBttvEmoteFixedStrings.clear();
 }
 
 void IrcChat::join(const QString channel, const QString channelId) {
@@ -178,6 +196,8 @@
 
     replayChatCurrentSeekOffset = playbackOffset;
 
+    replayChatFirstLoadAfterSeek = true;
+
     // Get start timestamp of the chat replay segments as the requests need to 
be aligned to chunk boundaries from this start
     _cman->getVodStartTime(vodId);
 }
@@ -194,19 +214,25 @@
 
 const double CHAT_CHUNK_TIME = 30.0;
 
+// When seeking, request past chat starting this far back in seconds...
+const double SEEK_HISTORY_TIME = 90.0;
+// ... and display up to this many messages of it.
+const int SEEK_HISTORY_MESSAGE_LIMIT = 15;
+
 void IrcChat::replaySeek(double newOffset) {
     // we set a flag indicating that a request is in flight
     if (replayChatRequestInProgress) {
         _cman->cancelLastVodChatRequest();
     }
     _cman->resetVodChat();
+    replayChatFirstLoadAfterSeek = true;
     replayChatRequestInProgress = true;
-    // we save the offset as the current time
-    replayChatCurrentTime = replayChatVodStartTime + newOffset;
+    // we save the offset to the point we want to start loading chat at as the 
current time
+    replayChatCurrentTime = replayChatVodStartTime + qMax(newOffset - 
SEEK_HISTORY_TIME, 0.0);
     qDebug() << "original vod playback start time" << 
QDateTime::fromMSecsSinceEpoch(replayChatCurrentTime * 1000.0, Qt::UTC);
     nextChatChunkTimestamp = quantize(replayChatCurrentTime, 
replayChatFirstChunkTime, CHAT_CHUNK_TIME);
     qDebug() << "quantized vod playback start time for chat chunk" << 
QDateTime::fromMSecsSinceEpoch(nextChatChunkTimestamp * 1000.0, Qt::UTC);
-    // we dump any pending messages from other stuff
+    // we dump any pending messages from the previous playback position
     replayChatMessagesPending.clear();
     // we'll do an initial request for starting offset chat right now.
     _cman->getVodChatPiece(replayVodId, nextChatChunkTimestamp);
@@ -309,29 +335,55 @@
 
     double curVideoOffsetMS = (replayChatCurrentTime - replayChatVodStartTime) 
* 1000.0;
 
-    // in a chat update check, we look at the current time emit whatever 
chat's timestamps are up
-    while (!replayChatMessagesPending.empty() && 
(replayChatMessagesPending.first().videoOffset) <= curVideoOffsetMS) {
-        auto message = replayChatMessagesPending.first();
-        auto delay = (curVideoOffsetMS - 
(replayChatMessagesPending.first().videoOffset)) / 1000.0;
+    double nextChatTime = nextChatChunkTimestamp;
+    // if we're close enough to the end of the chat buffer, we need more chat 
right away
+    bool needMoreChat = replayChatCurrentTime > (nextChatTime - 
CHAT_TIME_MARGIN);
+
+    // if it's the first set of replay chunks after seeking, wait until 
loading has caught up and then apply SEEK_HISTORY_MESSAGE_LIMIT across all the
+    // messages up to the current time from all the chunks
+    if (replayChatFirstLoadAfterSeek && !needMoreChat) {
+        int chatLinesReadyToOutput = 0;
+        for (const auto & entry : replayChatMessagesPending) {
+            if (entry.videoOffset > curVideoOffsetMS) {
+                break;
+            }
+            chatLinesReadyToOutput++;
+        }
+        qDebug() << "first load after seek; have" << chatLinesReadyToOutput << 
"current lines";
 
-        if (delay > 1.0 || delay < -1.0) {
-            qDebug() << 
"**********************************************************";
-            qDebug() << "chat replay delay" << delay << "s -" << message.from 
<< message.message;
-            qDebug() << 
"**********************************************************";
+        int removeCount = chatLinesReadyToOutput - SEEK_HISTORY_MESSAGE_LIMIT;
+        if (removeCount > 0) {
+            qDebug() << "skipping" << removeCount << "lines";
+            replayChatMessagesPending.erase(replayChatMessagesPending.begin(), 
replayChatMessagesPending.begin() + removeCount);
         }
 
-        replayChatMessage(message);
-        replayChatMessagesPending.pop_front();
+    } 
+
+    if (!replayChatFirstLoadAfterSeek || !needMoreChat) {
+        // look at the current time and emit whatever chat messages' 
timestamps are up
+        while (!replayChatMessagesPending.empty() && 
(replayChatMessagesPending.first().videoOffset) <= curVideoOffsetMS) {
+            auto message = replayChatMessagesPending.first();
+            auto delay = (curVideoOffsetMS - 
(replayChatMessagesPending.first().videoOffset)) / 1000.0;
+
+            if ((delay > 1.0 || delay < -1.0) && 
!replayChatFirstLoadAfterSeek) {
+                qDebug() << 
"**********************************************************";
+                qDebug() << "chat replay delay" << delay << "s -" << 
message.from << message.message;
+                qDebug() << 
"**********************************************************";
+            }
+
+            replayChatMessage(message);
+            replayChatMessagesPending.pop_front();
+        }
+
+        if (replayChatFirstLoadAfterSeek) {
+            replayChatFirstLoadAfterSeek = false;
+        }
     }
 
-    // there is some sketchy logic to figure out what timestamp we actually 
deem ourselves to have chat up until
-    double nextChatTime = nextChatChunkTimestamp;
-    // then if we're close enough to the end of the chat buffer and there 
isn't a request in progress, we request some more chat
-    if (!replayChatRequestInProgress && replayChatCurrentTime > (nextChatTime 
- CHAT_TIME_MARGIN)) {
+    if (!replayChatRequestInProgress && needMoreChat) {
         replayChatRequestInProgress = true;
         _cman->getVodChatPiece(replayVodId, nextChatTime);
     }
-        
 }
 
 void IrcChat::handleDownloadedReplayChat(QList<ReplayChatMessage> messages) {
@@ -424,17 +476,35 @@
 
     for (auto word = message.begin(); word != message.end(); word++) {
         bool spacePrefix = word != message.begin();
-        QString emoteText = spacePrefix ? word->toString().mid(1) : 
word->toString();
-        auto entry = relevantEmotes.find(emoteText);
-        if (entry != relevantEmotes.end()) {
+        QString possibleEmoteText = spacePrefix ? word->toString().mid(1) : 
word->toString();
+        bool isEmote = false;
+
+        auto entry = relevantEmotes.constFind(possibleEmoteText);
+        if (entry != relevantEmotes.constEnd()) {
             QString emoteId = entry.value().toString();
             _emoteProvider.makeAvailable(emoteId);
             if (spacePrefix) {
                 output.append(" ");
             }
-            
output.append(createImageEntry(_emoteProvider.getImageProviderName(), emoteId, 
emoteText));
+            
output.append(createImageEntry(_emoteProvider.getImageProviderName(), emoteId, 
possibleEmoteText));
+            isEmote = true;
         }
-        else {
+
+        for (const auto & emoteIndex : { lastCurChannelBttvEmoteFixedStrings, 
lastGlobalBttvEmoteFixedStrings }) {
+            auto entry = emoteIndex.constFind(possibleEmoteText);
+            if (entry != emoteIndex.constEnd()) {
+                QString emoteId = entry.value();
+                _bttvEmoteProvider.makeAvailable(emoteId);
+                if (spacePrefix) {
+                    output.append(" ");
+                }
+                
output.append(createImageEntry(_bttvEmoteProvider.getImageProviderName(), 
emoteId, possibleEmoteText));
+                isEmote = true;
+                break;
+            }
+        }
+
+        if (!isEmote) {
             output.append(*word);
         }
     }
@@ -677,10 +747,6 @@
        }
 }
 
-bool IrcChat::allDownloadsComplete() {
-    return !_emoteProvider.downloadsInProgress() && _badgeProvider && 
!_badgeProvider->downloadsInProgress() && _bitsProvider && 
!_bitsProvider->downloadsInProgress();
-}
-
 void IrcChat::disposeOfMessage(ChatMessage m) {
     if (allDownloadsComplete()) {
         emit messageReceived(m.name, m.messageList, m.color, m.subscriber, 
m.turbo, m.mod, m.isAction, m.badges, m.isChannelNotice, m.systemMessage, 
m.isWhisper);
@@ -836,6 +902,15 @@
     }
 }
 
+void IrcChat::handleBttvEmote(const QString & id, ImagePositionsMap & 
mapToUpdate, int pos, int end) {
+    InlineImageInfo info;
+    info.kind = ImageEntryKind::bttvEmote;
+
+    info.key = _bttvEmoteProvider.getCanonicalKey(id);
+    mapToUpdate.insert(pos, qMakePair(end, info));
+    _bttvEmoteProvider.makeAvailable(id);
+}
+
 void updateBitsRegexes(const BitsQStringsMap & bitsUrls, QMap<QString, 
QRegExp> & mapToUpdate) {
     mapToUpdate.clear();
     
@@ -886,7 +961,31 @@
         }
     }
 
+    // check for space-separated fixed string tokens
     int cur = 0;
+    while (cur < message.length()) {
+        int wordEnd = message.indexOf(' ', cur);
+        if (wordEnd == -1) {
+            wordEnd = message.length();
+        }
+
+        if (wordEnd > cur) {
+            const auto word = message.mid(cur, wordEnd - cur);
+            
+            for (const QMap<QString, QString> & bttvIndex : { 
lastCurChannelBttvEmoteFixedStrings, lastGlobalBttvEmoteFixedStrings }) {
+                const auto matchEntry = bttvIndex.constFind(word);
+                if (matchEntry != bttvIndex.constEnd()) {
+                    handleBttvEmote(matchEntry.value(), imagePositionsMap, 
cur, wordEnd);
+                    break;
+                }
+            }
+        }
+
+        cur = wordEnd + 1;
+    }
+
+    // go through all text replacement image entries and cut up the input 
message
+    cur = 0;
     for (auto i = imagePositionsMap.constBegin(); i != 
imagePositionsMap.constEnd(); i++) {
         auto emoteStart = i.key();
         if (emoteStart > cur) {
@@ -900,25 +999,31 @@
         QString originalText = message.mid(emoteStart, emoteAfterEnd - 
emoteStart);
 
         QVariantMap imgEntry;
+        bool doInsert = true;
 
         switch (imageKind) {
         case ImageEntryKind::emote:
             imgEntry = createImageEntry(_emoteProvider.getImageProviderName(), 
imageId, originalText);
-            imgEntry.insert("textSuffix", imageInfo.textSuffix);
-            imgEntry.insert("textSuffixColor", imageInfo.textSuffixColor);
-            messageList.append(imgEntry);
+            break;
+        case ImageEntryKind::bttvEmote:
+            imgEntry = 
createImageEntry(_bttvEmoteProvider.getImageProviderName(), imageId, 
originalText);
             break;
         case ImageEntryKind::bits:
             if (_bitsProvider) {
                 imgEntry = 
createImageEntry(_bitsProvider->getImageProviderName(), imageId, originalText);
                 // currently QML AnimatedImage doesn't support using images 
from a QQuickImageProvider
                 imgEntry.insert("sourceUrl", 
_cman->getBitsUrlForKey(imageId).toString());
-                imgEntry.insert("textSuffix", imageInfo.textSuffix);
-                imgEntry.insert("textSuffixColor", imageInfo.textSuffixColor);
-                messageList.append(imgEntry);
+            }
+            else {
+                doInsert = false;
             }
             break;
         }
+        if (doInsert) {
+            imgEntry.insert("textSuffix", imageInfo.textSuffix);
+            imgEntry.insert("textSuffixColor", imageInfo.textSuffixColor);
+            messageList.append(imgEntry);
+        }
         cur = emoteAfterEnd;
     }
     if (cur < message.length()) {
@@ -1232,6 +1337,22 @@
     _emoteProvider.bulkDownload(keys);
 }
 
+QList<QString> valuesList(const QMap<QString, QString> & map) {
+    QList<QString> out;
+    for (auto entry = map.constBegin(); entry != map.constEnd(); entry++) {
+        out.append(entry.value());
+    }
+    return out;
+}
+
+void IrcChat::downloadBttvEmotesGlobal() {
+    
_bttvEmoteProvider.bulkDownload(valuesList(lastGlobalBttvEmoteFixedStrings));
+}
+
+void IrcChat::downloadBttvEmotesChannel() {
+    
_bttvEmoteProvider.bulkDownload(valuesList(lastCurChannelBttvEmoteFixedStrings));
+}
+
 void IrcChat::blockedUsersLoaded(const QSet<QString> & newBlockedUsers) {
     blockedUsers = newBlockedUsers;
 }
@@ -1255,3 +1376,25 @@
         blockedUsers.remove(newUnblockedUsername);
     }
 }
+
+template <typename U>
+QVariantMap toVariantMap(const QMap<QString, U> & map) {
+    QVariantMap out;
+    for (auto entry = map.constBegin(); entry != map.constEnd(); entry++) {
+        out.insert(entry.key(), entry.value());
+    }
+    return out;
+}
+
+void IrcChat::handleChannelBttvEmotesLoaded(const QString & channelName, 
QMap<QString, QString> emotesByCode) {
+    const QString GLOBAL_EMOTES_ID = "GLOBAL";
+    if (channelName == GLOBAL_EMOTES_ID) {
+        lastGlobalBttvEmoteFixedStrings = emotesByCode;
+    }
+    else if (channelName == room) {
+        lastCurChannelBttvEmoteFixedStrings = emotesByCode;
+    }
+
+    emit bttvEmotesLoaded(channelName, toVariantMap(emotesByCode));
+}
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/model/ircchat.h 
new/orion-1.5.1+git~20170602/src/model/ircchat.h
--- old/orion-1.5.1+git~20170519/src/model/ircchat.h    2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/model/ircchat.h    2017-06-02 
07:42:07.000000000 +0200
@@ -119,6 +119,8 @@
     bool downloadError();
 
     void bulkDownloadComplete();
+
+    void bttvEmotesLoaded(QString channel, QVariantMap emotesByCode);
     
 public slots:
     void sendMessage(const QString &msg, const QVariantMap &relevantEmotes);
@@ -126,7 +128,8 @@
     void login();
 
     void bulkDownloadEmotes(QList<QString> keys);
-
+    void downloadBttvEmotesGlobal();
+    void downloadBttvEmotesChannel();
 private slots:
     void receive();
     void processError(QAbstractSocket::SocketError socketError);
@@ -139,6 +142,8 @@
     void userBlocked(const QString & blockedUsername);
     void userUnblocked(const QString & unblockedUsername);
 
+    void handleChannelBttvEmotesLoaded(const QString & channelName, 
QMap<QString, QString> emotesByCode);
+
 private:
     static bool hiDpi;
 
@@ -148,9 +153,13 @@
     static const QString IMAGE_PROVIDER_EMOTE;
     static const QString EMOTICONS_URL_FORMAT_HIDPI;
     static const QString EMOTICONS_URL_FORMAT_LODPI;
+    static const QString IMAGE_PROVIDER_BTTV_EMOTE;
+    static const QString BTTV_EMOTES_URL_FORMAT_HIDPI;
+    static const QString BTTV_EMOTES_URL_FORMAT_LODPI;
     static const QString IMAGE_PROVIDER_BITS;
 
     URLFormatImageProvider _emoteProvider;
+    URLFormatImageProvider _bttvEmoteProvider;
     BitsImageProvider * _bitsProvider;
     BadgeImageProvider * _badgeProvider;
     ChannelManager * _cman;
@@ -196,6 +205,7 @@
     bool allDownloadsComplete();
 
     bool replayChatRequestInProgress;
+    bool replayChatFirstLoadAfterSeek;
     
     double replayChatVodStartTime;
     double replayChatFirstChunkTime;
@@ -212,7 +222,10 @@
     QMap<QString, QRegExp> lastCurChannelBitsRegexes;
     QMap<QString, QRegExp> lastGlobalBitsRegexes;
 
-    enum ImageEntryKind { emote, bits };
+    QMap<QString, QString> lastGlobalBttvEmoteFixedStrings;
+    QMap<QString, QString> lastCurChannelBttvEmoteFixedStrings;
+
+    enum ImageEntryKind { emote, bits, bttvEmote };
 
     struct InlineImageInfo {
         ImageEntryKind kind;
@@ -225,6 +238,8 @@
 
     void checkBitsRegex(const QRegExp & regex, const QString & prefix, const 
QString & message, ImagePositionsMap & mapToUpdate);
 
+    void handleBttvEmote(const QString & id, ImagePositionsMap & mapToUpdate, 
int pos, int end);
+
     void roomInitCommon(const QString channel, const QString channelId);
 
     void setUserBlock(const QString & username, const bool blocked);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/orion-1.5.1+git~20170519/src/network/networkmanager.cpp 
new/orion-1.5.1+git~20170602/src/network/networkmanager.cpp
--- old/orion-1.5.1+git~20170519/src/network/networkmanager.cpp 2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/network/networkmanager.cpp 2017-06-02 
07:42:07.000000000 +0200
@@ -553,11 +553,11 @@
 
     QByteArray data = reply->readAll();
 
-    QList<QString> ret = JsonParser::parseBlockList(data);
+    auto result = JsonParser::parseBlockList(data);
 
     int nextOffset = reply->request().attribute(QNetworkRequest::User).toInt();
 
-    emit blockedUserListLoadOperationFinished(ret, nextOffset);
+    emit blockedUserListLoadOperationFinished(result.items, nextOffset, 
result.total);
 
     reply->deleteLater();
 }
@@ -831,6 +831,66 @@
     reply->deleteLater();
 }
 
+void NetworkManager::getChannelBttvEmotes(const QString channel) {
+    QString url = QString(BTTV_API) + QString("/channels/") + 
QUrl::toPercentEncoding(channel);
+
+    qDebug() << "Requesting" << url;
+
+    QNetworkRequest request;
+    request.setUrl(QUrl(url));
+
+    QNetworkReply *reply = operation->get(request);
+
+    connect(reply, &QNetworkReply::finished, this, 
&NetworkManager::channelBttvEmotesReply);
+}
+
+void NetworkManager::channelBttvEmotesReply() {
+    QNetworkReply* reply = qobject_cast<QNetworkReply *>(sender());
+
+    if (!handleNetworkError(reply)) {
+        return;
+    }
+    QByteArray data = reply->readAll();
+
+    auto url = reply->url();
+    QString urlString = url.toString();
+    QString channel = urlString.mid(urlString.lastIndexOf("/") + 1);
+
+    auto emotes = JsonParser::parseBttvEmotesData(data);
+
+    emit getChannelBttvEmotesOperationFinished(channel, emotes);
+
+    reply->deleteLater();
+}
+
+void NetworkManager::getGlobalBttvEmotes() {
+    QString url = QString(BTTV_API) + QString("/emotes");
+
+    qDebug() << "Requesting" << url;
+
+    QNetworkRequest request;
+    request.setUrl(QUrl(url));
+
+    QNetworkReply *reply = operation->get(request);
+
+    connect(reply, &QNetworkReply::finished, this, 
&NetworkManager::globalBttvEmotesReply);
+}
+
+void NetworkManager::globalBttvEmotesReply() {
+    QNetworkReply* reply = qobject_cast<QNetworkReply *>(sender());
+
+    if (!handleNetworkError(reply)) {
+        return;
+    }
+    QByteArray data = reply->readAll();
+
+    auto emotes = JsonParser::parseBttvEmotesData(data);
+
+    emit getGlobalBttvEmotesOperationFinished(emotes);
+
+    reply->deleteLater();
+}
+
 void NetworkManager::editUserFavourite(const QString &access_token, const 
quint64 userId, const quint64 channelId, bool add)
 {
     QString url = QString(KRAKEN_API) + "/users/" + QString::number(userId)
@@ -974,11 +1034,11 @@
         addULongLongStringList(queriedChannelIds, 
query.queryItemValue("channel").split(","));
     }
 
-    QList<Channel *> out = JsonParser::parseStreams(data);
+    PagedResult<Channel *> out = JsonParser::parseStreams(data);
 
-    addOfflineChannels(out, queriedChannelIds);
+    addOfflineChannels(out.items, queriedChannelIds);
 
-    emit allStreamsOperationFinished(out);
+    emit allStreamsOperationFinished(out.items);
 
     reply->deleteLater();
 }
@@ -1030,11 +1090,11 @@
         addULongLongStringList(queriedChannelIds, 
query.queryItemValue("channel").split(","));
     }
 
-    QList<Channel *> out = JsonParser::parseStreams(data);
+    PagedResult<Channel *> out = JsonParser::parseStreams(data);
 
-    addOfflineChannels(out, queriedChannelIds);
+    addOfflineChannels(out.items, queriedChannelIds);
 
-    emit gameStreamsOperationFinished(out);
+    emit gameStreamsOperationFinished(out.items, out.total);
 
     reply->deleteLater();
 }
@@ -1066,7 +1126,8 @@
 
     //qDebug() << data;
 
-    emit searchChannelsOperationFinished(JsonParser::parseChannels(data));
+    auto result = JsonParser::parseChannels(data);
+    emit searchChannelsOperationFinished(result.items, result.total);
 
     reply->deleteLater();
 }
@@ -1156,7 +1217,8 @@
 
     int offset = reply->request().attribute(QNetworkRequest::User).toInt();
 
-    emit favouritesReplyFinished(JsonParser::parseFavourites(data), offset);
+    auto result = JsonParser::parseFavourites(data);
+    emit favouritesReplyFinished(result.items, offset, result.total);
 
     reply->deleteLater();
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/orion-1.5.1+git~20170519/src/network/networkmanager.h 
new/orion-1.5.1+git~20170602/src/network/networkmanager.h
--- old/orion-1.5.1+git~20170519/src/network/networkmanager.h   2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/network/networkmanager.h   2017-06-02 
07:42:07.000000000 +0200
@@ -81,6 +81,8 @@
     void getGlobalBadgesUrlsBeta();
     void getChannelBitsUrls(const int channelID);
     void getGlobalBitsUrls();
+    void getChannelBttvEmotes(const QString channel);
+    void getGlobalBttvEmotes();
 
     Q_INVOKABLE void getVodStartTime(quint64 vodId);
     Q_INVOKABLE void getVodChatPiece(quint64 vodId, quint64 offset);
@@ -101,15 +103,15 @@
 
     void allStreamsOperationFinished(const QList<Channel *>&);
     void gamesOperationFinished(const QList<Game *>&);
-    void gameStreamsOperationFinished(const QList<Channel *>&);
+    void gameStreamsOperationFinished(const QList<Channel *>&, const int 
total);
     void featuredStreamsOperationFinished(const QList<Channel *>&);
-    void searchChannelsOperationFinished(const QList<Channel *>&);
+    void searchChannelsOperationFinished(const QList<Channel *>&, const int 
total);
     void searchGamesOperationFinished(const QList<Game *>&);
     void broadcastsOperationFinished(const QList<Vod *>&);
     void m3u8OperationFinished(const QVariantMap&);
     void m3u8OperationBFinished(const QVariantMap&);
     void fileOperationFinished(const QByteArray&);
-    void favouritesReplyFinished(const QList<Channel *>&, const quint32);
+    void favouritesReplyFinished(const QList<Channel *>&, const quint32, const 
quint32);
     void streamGetOperationFinished(const quint64 channelId, const bool 
online);
     void error(const QString &error);
 
@@ -124,11 +126,14 @@
     void vodStartGetOperationFinished(double);
     void vodChatPieceGetOperationFinished(QList<ReplayChatMessage>);
     void chatterListLoadOperationFinished(QMap<QString, QList<QString>>);
-    void blockedUserListLoadOperationFinished(QList<QString>, const quint32 
nextOffset);
+    void blockedUserListLoadOperationFinished(QList<QString>, const quint32 
nextOffset, const quint32 total);
 
     void getChannelBitsUrlsOperationFinished(int channelID, BitsQStringsMap 
channelBitsUrls, BitsQStringsMap channelBitsColors);
     void getGlobalBitsUrlsOperationFinished(BitsQStringsMap globalBitsUrls, 
BitsQStringsMap globalBitsColors);
 
+    void getChannelBttvEmotesOperationFinished(const QString channel, 
QMap<QString, QString> & emotesByCode);
+    void getGlobalBttvEmotesOperationFinished(QMap<QString, QString> & 
emotesByCode);
+
     void networkAccessChanged(bool up);
 
     void userBlocked(quint64 myUserId, const QString & blockedUsername);
@@ -166,6 +171,8 @@
     void channelBitsUrlsReply();
     void globalBitsUrlsReply();
     void blockUserLookupReply();
+    void globalBttvEmotesReply();
+    void channelBttvEmotesReply();
 
 private:
     static const QString CHANNEL_BADGES_URL_PREFIX;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/network/urls.h 
new/orion-1.5.1+git~20170602/src/network/urls.h
--- old/orion-1.5.1+git~20170519/src/network/urls.h     2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/network/urls.h     2017-06-02 
07:42:07.000000000 +0200
@@ -20,6 +20,7 @@
 #define TWITCH_RECHAT_API "https://rechat.twitch.tv/rechat-messages";
 #define TWITCH_TMI_USER_API "https://tmi.twitch.tv/group/user/";
 //#define TWITCH_EMOTES "http://static-cdn.jtvnw.net/emoticons/v1/";
+#define BTTV_API "https://api.betterttv.net/2";
 #define CLIENT_ID "0dpzlnp1w2bjlim3ldp0u96o4dq2gm"
 
 #endif // URLS_H
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/qml/SearchView.qml 
new/orion-1.5.1+git~20170602/src/qml/SearchView.qml
--- old/orion-1.5.1+git~20170519/src/qml/SearchView.qml 2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/qml/SearchView.qml 2017-06-02 
07:42:07.000000000 +0200
@@ -65,7 +65,7 @@
 
             _spinner.visible = false
             _button.visible = true
-            channels.checkScrolled()
+            channels.checkScrolled(total)
         }
 
         onSearchingStarted: {
@@ -191,7 +191,10 @@
 
         model: g_results
 
-        function checkScrolled(){
+        function checkScrolled(total){
+            if (total != null && itemCount >= total) {
+                return;
+            }
             if (atYEnd && model.count() === itemCount && itemCount > 0){
                 search(_input.text, itemCount, 25, false);
                 itemCount += 25
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/qml/irc/Chat.qml 
new/orion-1.5.1+git~20170602/src/qml/irc/Chat.qml
--- old/orion-1.5.1+git~20170519/src/qml/irc/Chat.qml   2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/qml/irc/Chat.qml   2017-06-02 
07:42:07.000000000 +0200
@@ -28,6 +28,7 @@
     signal bulkDownloadComplete()
     signal channelBadgeUrlsLoaded(int channelId, var badgeUrls)
     signal channelBadgeBetaUrlsLoaded(string channel, var badgeSetData)
+    signal bttvEmotesLoaded(string channel, var emotesByCode)
 
     property alias isAnonymous: chat.anonymous
     property var channel: undefined
@@ -72,6 +73,7 @@
         g_cman.loadChannelBadgeUrls(channelId);
         g_cman.loadChannelBetaBadgeUrls(channelId);
         g_cman.loadChannelBitsUrls(channelId);
+        g_cman.loadChannelBttvEmotes(channelName);
     }
 
     function joinChannel(channelName, channelId) {
@@ -123,6 +125,14 @@
         return chat.bulkDownloadEmotes(emotes);
     }
 
+    function downloadBttvEmotesGlobal() {
+        return chat.downloadBttvEmotesGlobal();
+    }
+
+    function downloadBttvEmotesChannel() {
+        return chat.downloadBttvEmotesChannel();
+    }
+
     function reconnect() {
         leaveChannel()
         if (root.channel)
@@ -168,5 +178,9 @@
         onBulkDownloadComplete: {
             root.bulkDownloadComplete();
         }
+
+        onBttvEmotesLoaded: {
+            root.bttvEmotesLoaded(channel, emotesByCode);
+        }
     }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/qml/irc/ChatView.qml 
new/orion-1.5.1+git~20170602/src/qml/irc/ChatView.qml
--- old/orion-1.5.1+git~20170519/src/qml/irc/ChatView.qml       2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/qml/irc/ChatView.qml       2017-06-02 
07:42:07.000000000 +0200
@@ -58,22 +58,30 @@
         }
     }
 
+    function cleanupPrevChannel() {
+        if (chat.lastBttvChannelEmotes != null) {
+            _emoteButton.clearChannelSpecificEmotes()
+            chat.lastBttvChannelEmotes = null;
+        }
+        chatModel.clear()
+    }
+
     function joinChannel(channel, channelId) {
         if (channel !== chat.channel || chat.replayMode) {
             viewerListEnabled = false;
-            chatModel.clear()
+            cleanupPrevChannel()
             chat.joinChannel(channel, channelId)
         }
     }
 
     function leaveChannel() {
-        chatModel.clear()
+        cleanupPrevChannel()
         chat.leaveChannel()
     }
 
     function replayChat(channelName, channelId, vodId, startEpochTime) {
         viewerListEnabled = false;
-        chatModel.clear()
+        cleanupPrevChannel()
         chat.leaveChannel()
         chat.replayChat(channelName, channelId, vodId, startEpochTime);
     }
@@ -118,12 +126,20 @@
 
     function loadEmotes() {
         //console.log("loadEmotes()", chat.lastEmoteSets);
-        if (chat.lastEmoteSets) {
+        if (!_emoteButton.pickerLoaded) {
+            if (chat.lastEmoteSets) {
+                _emotePicker.loading = true;
+                //console.log("downloading chat.lastEmoteSets", 
chat.lastEmoteSets);
+                _emoteButton.startDownload(chat.lastEmoteSets);
+            }
+        } else if (_emoteButton.pickerChannelLoaded != chat.channel) {
+            // just download channel emotes we don't have yet
             _emotePicker.loading = true;
-            //console.log("downloading chat.lastEmoteSets", 
chat.lastEmoteSets);
-            _emoteButton.startDownload(chat.lastEmoteSets);
+            _emoteButton.startDownload(null);
         }
+
         _emoteButton.pickerLoaded = true;
+        _emoteButton.pickerChannelLoaded = chat.channel;
     }
 
     Connections {
@@ -548,6 +564,7 @@
                 property ListModel setsVisible: ListModel { }
 
                 property bool pickerLoaded: false
+                property var pickerChannelLoaded: null
 
                 visible: root.chatViewVisible
 
@@ -564,7 +581,7 @@
                 Connections {
                     target: _emotePicker
                     onVisibleChanged: {
-                        if (_emotePicker.visible && 
!_emoteButton.pickerLoaded) {
+                        if (_emotePicker.visible) {
                             loadEmotes();
                         }
                     }
@@ -744,27 +761,65 @@
 
                 function showLastSet() {
                     //console.log("showing last set", lastSet);
-                    var lastSetMap = lastEmoteSets[lastSet];
-                    for (var i in lastSetMap) {
-                        setsVisible.append({"imageUrl": "image://emote/" + i, 
"emoteName": decodeHtml(inverseRegex(lastSetMap[i]))})
+                    switch(lastSet) {
+                    case "bttvGlobal":
+                        for (var i in chat.lastBttvGlobalEmotes) {
+                            setsVisible.append({"imageUrl": 
"image://bttvemote/" + chat.lastBttvGlobalEmotes[i], "emoteName": i});
+                        }
+                        break;
+                    case "bttvChannel":
+                        for (var i in chat.lastBttvChannelEmotes) {
+                            setsVisible.append({"imageUrl": 
"image://bttvemote/" + chat.lastBttvChannelEmotes[i], "emoteName": i});
+                        }
+                        break;
+                    default:
+                        var lastSetMap = lastEmoteSets[lastSet];
+                        for (var i in lastSetMap) {
+                            setsVisible.append({"imageUrl": "image://emote/" + 
i, "emoteName": decodeHtml(inverseRegex(lastSetMap[i]))})
+                        }
+                        break;
                     }
                     _emotePicker.updateFilter();
                 }
 
+                function clearChannelSpecificEmotes() {
+                    //console.log("clearChannelSpecificEmotes()")
+                    var channelEmotes = chat.lastBttvChannelEmotes;
+                    if (channelEmotes != null) {
+                        for (var i = 0; i < setsVisible.count; ) {
+                            var obj = setsVisible.get(i);
+                            if (channelEmotes.hasOwnProperty(obj.emoteName)) {
+                                //console.log("remove channel emote", 
obj.emoteName, i);
+                                setsVisible.remove(i);
+                            } else {
+                                i++;
+                            }
+                        }
+                    }
+                    _emoteButton.pickerChannelLoaded = null;
+                }
+
                 function nextDownload() {
                     if (emotePickerDownloadsInProgress) {
                         if (curDownloading < setsToDownload.length) {
                             var curSetID = setsToDownload[curDownloading];
                             lastSet = curSetID;
-                            var curSetMap = lastEmoteSets[curSetID];
-                            var curSetList = [];
-                            for (var i in curSetMap) {
-                                curSetList.push(i);
-                            }
                             curDownloading ++;
                             console.log("Downloading emote set #", 
curDownloading, curSetID);
-                            chat.bulkDownloadEmotes(curSetList);
+                            if (curSetID == "bttvGlobal") {
+                                chat.downloadBttvEmotesGlobal();
+                            } else if (curSetID == "bttvChannel") {
+                                chat.downloadBttvEmotesChannel();
+                            } else {
+                                var curSetMap = lastEmoteSets[curSetID];
+                                var curSetList = [];
+                                for (var i in curSetMap) {
+                                    curSetList.push(i);
+                                }
+                                chat.bulkDownloadEmotes(curSetList);
+                            }
                         } else {
+                            console.log("Emote set downloads complete");
                             emotePickerDownloadsInProgress = false;
                             _emotePicker.loading = false;
                         }
@@ -773,10 +828,16 @@
 
                 function startDownload(emoteSets) {
                     curDownloading = 0;
-                    lastEmoteSets = emoteSets;
                     setsToDownload = [];
-                    for (var i in emoteSets) {
-                        setsToDownload.push(i);
+                    if (emoteSets != null) {
+                        lastEmoteSets = emoteSets;
+                        for (var i in emoteSets) {
+                            setsToDownload.push(i);
+                        }
+                        setsToDownload.push("bttvGlobal");
+                    }
+                    if (chat.lastBttvChannelEmotes != null) {
+                        setsToDownload.push("bttvChannel");
                     }
                     //console.log("Starting download of emote sets", 
setsToDownload);
                     emotePickerDownloadsInProgress = true;
@@ -808,6 +869,8 @@
         property string emoteDirPath
         property variant lastEmoteSetIDs
         property variant lastEmoteSets
+        property variant lastBttvChannelEmotes
+        property variant lastBttvGlobalEmotes
 
         property variant _textEmotesMap
         property variant _regexEmotesList
@@ -823,6 +886,23 @@
             initEmotesMaps();
         }
 
+        onBttvEmotesLoaded: {
+            /*
+            console.log("received bttv emotes for", channel);
+            for (var i in emotesByCode) {
+                console.log("code", i, "id", emotesByCode[i]);
+            }
+            */
+
+            if (channel == "GLOBAL") {
+                chat.lastBttvGlobalEmotes = emotesByCode;
+            } else if (channel == chat.channel) {
+                chat.lastBttvChannelEmotes = emotesByCode;
+            } else {
+                console.log("bttv emotes loaded for a different channel", 
channel);
+            }
+        }
+
         function regexExactMatch(regex, text) {
             var match = regex.exec(text);
             return match && match[0] === text;
@@ -1031,7 +1111,7 @@
         }
 
         onClear: {
-            chatModel.clear()
+            cleanupPrevChannel()
         }
     }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/util/jsonparser.cpp 
new/orion-1.5.1+git~20170602/src/util/jsonparser.cpp
--- old/orion-1.5.1+git~20170519/src/util/jsonparser.cpp        2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/util/jsonparser.cpp        2017-06-02 
07:42:07.000000000 +0200
@@ -20,9 +20,9 @@
     hiDpi = setting;
 }
 
-QList<Channel*> JsonParser::parseStreams(const QByteArray &data)
+PagedResult<Channel*> JsonParser::parseStreams(const QByteArray &data)
 {
-    QList<Channel*> channels;
+    PagedResult<Channel*> out;
 
     QJsonParseError error;
     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
@@ -32,13 +32,15 @@
         //Online streams
         QJsonArray arr = json["streams"].toArray();
         foreach (const QJsonValue &item, arr){
-            channels.append(JsonParser::parseStreamJson(item.toObject(), 
true));
+            out.items.append(JsonParser::parseStreamJson(item.toObject(), 
true));
         }
 
+        out.total = json["_total"].toInt();
+
         //Caller must use request context to determine offline streams
     }
 
-    return channels;
+    return out;
 }
 
 Channel *JsonParser::parseStream(const QByteArray &data)
@@ -252,9 +254,9 @@
     return vod;
 }
 
-QList<Channel*> JsonParser::parseChannels(const QByteArray &data)
+PagedResult<Channel*> JsonParser::parseChannels(const QByteArray &data)
 {
-    QList<Channel*> channels;
+    PagedResult<Channel*> out;
 
     QJsonParseError error;
     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
@@ -263,29 +265,33 @@
 
         QJsonArray arr = json["channels"].toArray();
         foreach (const QJsonValue &item, arr){
-            channels.append(JsonParser::parseChannelJson(item.toObject()));
+            out.items.append(JsonParser::parseChannelJson(item.toObject()));
         }
+
+        out.total = json["_total"].toInt();
     }
 
-    return channels;
+    return out;
 }
 
-QList<Channel *> JsonParser::parseFavourites(const QByteArray &data)
+PagedResult<Channel *> JsonParser::parseFavourites(const QByteArray &data)
 {
-    QList<Channel*> channels;
+    PagedResult<Channel *> out;
 
     QJsonParseError error;
     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
     if (error.error == QJsonParseError::NoError){
         QJsonObject json = doc.object();
 
+        out.total = json["_total"].toInt();
+
         QJsonArray arr = json["follows"].toArray();
         foreach (const QJsonValue &item, arr){
-            
channels.append(JsonParser::parseChannelJson(item.toObject()["channel"].toObject()));
+            
out.items.append(JsonParser::parseChannelJson(item.toObject()["channel"].toObject()));
         }
     }
 
-    return channels;
+    return out;
 }
 
 QList<Channel *> JsonParser::parseFeatured(const QByteArray &data)
@@ -705,9 +711,9 @@
     
 }
 
-QList<QString> JsonParser::parseBlockList(const QByteArray &data)
+PagedResult<QString> JsonParser::parseBlockList(const QByteArray &data)
 {
-    QList<QString> out;
+    PagedResult<QString> out;
 
     QJsonParseError error;
     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
@@ -715,16 +721,44 @@
     if (error.error == QJsonParseError::NoError) {
         QJsonObject json = doc.object();
 
+        out.total = json["_total"].toInt();
+
         QJsonArray blocks = json["blocks"].toArray();
 
         for (const auto & block : blocks) {
             const auto & blockObj = block.toObject();
             const auto & name = blockObj["user"].toObject()["name"].toString();
             if (!name.isEmpty()) {
-                out.append(name);
+                out.items.append(name);
             }
         }
     }
 
     return out;
 }
+
+
+QMap<QString, QString> JsonParser::parseBttvEmotesData(const QByteArray &data)
+{
+    QMap<QString, QString> out;
+
+    QJsonParseError error;
+    QJsonDocument doc = QJsonDocument::fromJson(data, &error);
+
+    if (error.error == QJsonParseError::NoError) {
+        QJsonObject json = doc.object();
+
+        QJsonArray emotes = json["emotes"].toArray();
+
+        for (const auto & emote : emotes) {
+            const auto & emoteObj = emote.toObject();
+            const auto & id = emoteObj["id"].toString();
+            const auto & code = emoteObj["code"].toString();
+            if (!id.isEmpty() && !code.isEmpty()) {
+                out.insert(code, id);
+            }
+        }
+    }
+
+    return out;
+}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/orion-1.5.1+git~20170519/src/util/jsonparser.h 
new/orion-1.5.1+git~20170602/src/util/jsonparser.h
--- old/orion-1.5.1+git~20170519/src/util/jsonparser.h  2017-05-19 
14:24:40.000000000 +0200
+++ new/orion-1.5.1+git~20170602/src/util/jsonparser.h  2017-06-02 
07:42:07.000000000 +0200
@@ -31,13 +31,19 @@
  * Handles parsing of json documents to business logic-objects
  */
 
+template <typename U>
+struct PagedResult {
+    QList<U> items;
+    int total;
+};
+
 class JsonParser
 {
 public:
-    static QList<Channel*> parseStreams(const QByteArray&);
+    static PagedResult<Channel*> parseStreams(const QByteArray&);
     static QList<Game*> parseGames(const QByteArray&);
-    static QList<Channel*> parseChannels(const QByteArray&);
-    static QList<Channel*> parseFavourites(const QByteArray&);
+    static PagedResult<Channel*> parseChannels(const QByteArray&);
+    static PagedResult<Channel*> parseFavourites(const QByteArray&);
     static QList<Channel*> parseFeatured(const QByteArray&);
     static QList<Vod *> parseVods(const QByteArray&);
     static Game* parseGame(const QJsonObject&);
@@ -56,8 +62,9 @@
     static QMap<QString, QMap<QString, QMap<QString, QString>>> 
parseBadgeUrlsBetaFormat(const QByteArray &data);
     static QList<ReplayChatMessage> parseVodChatPiece(const QByteArray &data);
     static QMap<QString, QList<QString>> parseChatterList(const QByteArray 
&data);
-    static QList<QString> parseBlockList(const QByteArray &data);
+    static PagedResult<QString> parseBlockList(const QByteArray &data);
     static void parseBitsData(const QByteArray &data, QMap<QString, 
QMap<QString, QString>> & outUrls, QMap<QString, QMap<QString, QString>> & 
outColors);
+    static QMap<QString, QString> parseBttvEmotesData(const QByteArray &data);
     static void setHiDpi(bool setting);
 private:
     static bool hiDpi;


Reply via email to