Hello community, here is the log from the commit of package orion for openSUSE:Factory checked in at 2017-05-09 18:03:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/orion (Old) and /work/SRC/openSUSE:Factory/.orion.new (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "orion" Tue May 9 18:03:09 2017 rev:3 rq:493303 version:1.5.1+git~20170503 Changes: -------- --- /work/SRC/openSUSE:Factory/orion/orion.changes 2017-04-29 10:54:20.233337545 +0200 +++ /work/SRC/openSUSE:Factory/.orion.new/orion.changes 2017-05-09 18:03:15.272005918 +0200 @@ -1,0 +2,36 @@ +Thu May 04 07:30:56 UTC 2017 - [email protected] + +- Update to version 1.5.1+git~20170503: + * reduce download rate to make video impact less severe + * disable some badge debug output + * avoid downloadComplete signal in the middle of a download batch; don't count already-loaded images toward the throttling + * simplify timing + * switch to QTimer-based download start rate so we're not spinning on processEvents + * move ImageProvider bulk download completion to a separate signal rather having a bunch of special logic to overload downloadComplete + * remove unused bulk download returned value logic + +------------------------------------------------------------------- +Mon May 01 10:22:03 UTC 2017 - [email protected] + +- Update to version 1.5.1+git~20170501: + * add /w for whispers; accept any case for slash commands + * fix ChatView anchor changes unintentional width setting + * hide chat controls in small mode + * hide chat List when chat hidden + * fix chat controls position with swapped chat + * fixed transparent bg on chat toolbar + * tweaks to viewerlist + +------------------------------------------------------------------- +Sun Apr 30 10:30:15 UTC 2017 - [email protected] + +- Update to version 1.5.1+git~20170429: + * if a chat replay is in progress don't rejoin live chat when the network recovers + * receive and send whispers from the chat view + * add viewer list to chat + * move viewer list button to top navbar + * viewer list tooltip + * disable viewer list button when there is no current channel in playerview + * Moved list button inside chatview + +------------------------------------------------------------------- Old: ---- orion-1.5.1+git~20170428.tar.xz New: ---- orion-1.5.1+git~20170503.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ orion.spec ++++++ --- /var/tmp/diff_new_pack.kzK5aX/_old 2017-05-09 18:03:16.243868583 +0200 +++ /var/tmp/diff_new_pack.kzK5aX/_new 2017-05-09 18:03:16.247868018 +0200 @@ -17,7 +17,7 @@ Name: orion -Version: 1.5.1+git~20170428 +Version: 1.5.1+git~20170503 Release: 0 Summary: Twitch stream client using Qt License: GPL-3.0 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.kzK5aX/_old 2017-05-09 18:03:16.315858410 +0200 +++ /var/tmp/diff_new_pack.kzK5aX/_new 2017-05-09 18:03:16.315858410 +0200 @@ -1,4 +1,4 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/alamminsalo/orion.git</param> - <param name="changesrevision">0aa0e759ddec285231d41f8b9d6be124f112a60a</param></service></servicedata> \ No newline at end of file + <param name="changesrevision">91d02947a15ccf8d4086461fb3d86fc8771fac9f</param></service></servicedata> \ No newline at end of file ++++++ orion-1.5.1+git~20170428.tar.xz -> orion-1.5.1+git~20170503.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/channelmanager.cpp new/orion-1.5.1+git~20170503/src/model/channelmanager.cpp --- old/orion-1.5.1+git~20170428/src/model/channelmanager.cpp 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/channelmanager.cpp 2017-05-03 21:01:12.000000000 +0200 @@ -36,7 +36,7 @@ if (splitPos != -1) { const QString badge = key.left(splitPos); const QString version = key.mid(splitPos + 1); - qDebug() << "badge hunt: channel name" << _channelName << "channel id" << _channelId << "badge" << badge << "version" << version; + //qDebug() << "badge hunt: channel name" << _channelName << "channel id" << _channelId << "badge" << badge << "version" << version; if (_channelManager->getChannelBadgeBetaUrl(_channelId, badge, version, betaImageFormat, url)) { return QList<QString>({ _channelId, badge, version, betaImageFormat }).join("-"); @@ -128,6 +128,7 @@ connect(netman, SIGNAL(favouritesReplyFinished(const QList<Channel*>&, const quint32)), this, SLOT(addFollowedResults(const QList<Channel*>&, const quint32))); connect(netman, SIGNAL(vodStartGetOperationFinished(double)), this, SIGNAL(vodStartGetOperationFinished(double))); connect(netman, SIGNAL(vodChatPieceGetOperationFinished(QList<ReplayChatMessage>)), this, SIGNAL(vodChatPieceGetOperationFinished(QList<ReplayChatMessage>))); + connect(netman, SIGNAL(chatterListLoadOperationFinished(QMap<QString, QList<QString>>)), this, SLOT(processChatterList(QMap<QString, QList<QString>>))); connect(netman, SIGNAL(networkAccessChanged(bool)), this, SLOT(onNetworkAccessChanged(bool))); load(); @@ -733,6 +734,10 @@ return out; } +void ChannelManager::loadChatterList(const QString channel) { + netman->loadChatterList(channel); +} + void ChannelManager::getVodStartTime(quint64 vodId) { netman->getVodStartTime(vodId); } @@ -811,6 +816,20 @@ emit followedUpdated(); } +void ChannelManager::processChatterList(QMap<QString, QList<QString>> chatters) +{ + QVariantMap out; + for (auto groupEntry = chatters.constBegin(); groupEntry != chatters.constEnd(); groupEntry++) { + QVariantList group; + for (const auto & chatter : groupEntry.value()) { + group.append(chatter); + } + out.insert(groupEntry.key(), group); + } + + emit chatterListLoaded(out); +} + void ChannelManager::onNetworkAccessChanged(bool up) { if (up) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/channelmanager.h new/orion-1.5.1+git~20170503/src/model/channelmanager.h --- old/orion-1.5.1+git~20170428/src/model/channelmanager.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/channelmanager.h 2017-05-03 21:01:12.000000000 +0200 @@ -147,6 +147,7 @@ Q_INVOKABLE bool loadEmoteSets(bool reload, const QList<int> &emoteSetIDs); Q_INVOKABLE bool loadChannelBadgeUrls(const QString channel); Q_INVOKABLE bool loadChannelBetaBadgeUrls(int channel); + Q_INVOKABLE void loadChatterList(const QString channel); Q_INVOKABLE void cancelLastVodChatRequest(); Q_INVOKABLE void resetVodChat(); @@ -221,6 +222,8 @@ void vodStartGetOperationFinished(double); void vodChatPieceGetOperationFinished(QList<ReplayChatMessage>); + void chatterListLoaded(QVariantMap chatters); + public slots: void checkFavourites(); void addToFavourites(const quint32&); @@ -248,6 +251,7 @@ void innerGlobalBadgeBetaUrlsLoaded(const QMap<QString, QMap<QString, QMap<QString, QString>>> badgeData); void addFollowedResults(const QList<Channel*>&, const quint32); void onNetworkAccessChanged(bool); + void processChatterList(QMap<QString, QList<QString>> chatters); }; #endif //CHANNEL_MANAGER_H diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/imageprovider.cpp new/orion-1.5.1+git~20170503/src/model/imageprovider.cpp --- old/orion-1.5.1+git~20170428/src/model/imageprovider.cpp 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/imageprovider.cpp 2017-05-03 21:01:12.000000000 +0200 @@ -19,13 +19,21 @@ #include <QDebug> #include <QNetworkRequest> #include <QStandardPaths> +#include <QApplication> +#include <QDateTime> #include "imageprovider.h" +const int ImageProvider::MSEC_PER_DOWNLOAD = 16; // ~ 256kbit/sec for 2k images + ImageProvider::ImageProvider(const QString imageProviderName, const QString extension, const QString cacheDirName) : _imageProviderName(imageProviderName), _extension(extension) { activeDownloadCount = 0; + _bulkDownloadTimer.setInterval(MSEC_PER_DOWNLOAD); + _bulkDownloadTimer.setSingleShot(true); + connect(&_bulkDownloadTimer, SIGNAL(timeout()), this, SLOT(bulkDownloadStep())); + QString useCacheDirName = cacheDirName != "" ? cacheDirName : imageProviderName; _cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QString("/" + useCacheDirName); } @@ -55,7 +63,7 @@ bool ImageProvider::download(QString key) { if (_imageTable.contains(key)) { - qDebug() << "already in the table"; + //qDebug() << "already in the table"; return false; } @@ -89,14 +97,29 @@ return true; } -bool ImageProvider::bulkDownload(const QList<QString> & keys) { - bool waitForDownloadComplete = false; - for (const auto & key : keys) { +void ImageProvider::bulkDownloadStep() { + for (; _bulkDownloadPos != _curBulkDownloadKeys.constEnd(); _bulkDownloadPos++) { + const QString & key = *_bulkDownloadPos; + if (makeAvailable(key)) { - waitForDownloadComplete = true; + // hit us back when the next time interval is up + _bulkDownloadTimer.start(); + return; + } + else { + qApp->processEvents(); } } - return waitForDownloadComplete; + + emit bulkDownloadComplete(); +} + +void ImageProvider::bulkDownload(const QList<QString> & keys) { + _curBulkDownloadKeys = keys; + + _bulkDownloadPos = keys.constBegin(); + + bulkDownloadStep(); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/imageprovider.h new/orion-1.5.1+git~20170503/src/model/imageprovider.h --- old/orion-1.5.1+git~20170428/src/model/imageprovider.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/imageprovider.h 2017-05-03 21:01:12.000000000 +0200 @@ -24,6 +24,7 @@ #include <QSet> #include <QQuickImageProvider> #include <QNetworkReply> +#include <QTimer> // Handles state for an individual download class DownloadHandler : public QObject @@ -81,15 +82,21 @@ signals: void downloadComplete(); + void bulkDownloadComplete(); public slots: - bool bulkDownload(const QList<QString> & keys); + void bulkDownload(const QList<QString> & keys); void individualDownloadComplete(QString filename, bool hadError); +protected slots: + void bulkDownloadStep(); + protected: virtual const QUrl getUrlForKey(QString & key) = 0; private: + static const int MSEC_PER_DOWNLOAD; + QNetworkAccessManager _manager; QHash<QString, QImage*> _imageTable; @@ -98,6 +105,9 @@ int activeDownloadCount; QString _extension; QSet<QString> currentlyDownloading; + QTimer _bulkDownloadTimer; + QList<QString> _curBulkDownloadKeys; + QList<QString>::const_iterator _bulkDownloadPos; }; class URLFormatImageProvider : public ImageProvider { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/ircchat.cpp new/orion-1.5.1+git~20170503/src/model/ircchat.cpp --- old/orion-1.5.1+git~20170428/src/model/ircchat.cpp 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/ircchat.cpp 2017-05-03 21:01:12.000000000 +0200 @@ -43,12 +43,14 @@ _emoteProvider(IMAGE_PROVIDER_EMOTE, EMOTICONS_URL_FORMAT, ".png", "emotes"), _badgeProvider(nullptr), _cman(nullptr), - sock(nullptr) + sock(nullptr), + replayMode(false) { logged_in = false; connect(&_emoteProvider, &ImageProvider::downloadComplete, this, &IrcChat::handleDownloadComplete); + connect(&_emoteProvider, &ImageProvider::bulkDownloadComplete, this, &IrcChat::bulkDownloadComplete); room = ""; @@ -104,6 +106,7 @@ } void IrcChat::join(const QString channel, const QString channelId) { + replayMode = false; if (inRoom()) leave(); @@ -130,6 +133,8 @@ } void IrcChat::replay(const QString channel, const QString channelId, const quint64 vodId, double vodStartEpochTime, double playbackOffset) { + replayMode = true; + if (inRoom()) leave(); @@ -464,18 +469,42 @@ void IrcChat::sendMessage(const QString &msg, const QVariantMap &relevantEmotes) { if (inRoom() && connected()) { - if (sock) { - sock->write(("PRIVMSG #" + room + " :" + msg + "\r\n").toStdString().c_str()); - } - bool isAction = false; + bool isWhisper = false; + QString recipient = ""; QVariantList message; const QString ME_PREFIX = "/me "; QString displayMessage = msg; - if (displayMessage.startsWith(ME_PREFIX)) { + if (displayMessage.toLower().startsWith(ME_PREFIX)) { isAction = true; displayMessage = displayMessage.mid(ME_PREFIX.length()); } + for (const QString & prefix : { "/msg ", "/w " }) { + if (displayMessage.toLower().startsWith(prefix)) { + displayMessage = displayMessage.mid(prefix.length()); + isWhisper = true; + int spacePos = displayMessage.indexOf(' '); + if (spacePos == -1 || spacePos == displayMessage.length() - 1) { + emit noticeReceived("Ignoring whisper with empty message"); + return; + } + recipient = displayMessage.left(spacePos); + displayMessage = displayMessage.mid(spacePos + 1); + break; + } + } + + if (sock) { + QString ircCmd; + if (isWhisper) { + ircCmd = "PRIVMSG #" + room + " :/w " + recipient + " " + displayMessage + "\r\n"; + } + else { + ircCmd = "PRIVMSG #" + room + " :" + msg + "\r\n"; + } + sock->write(ircCmd.toStdString().c_str()); + } + addWordSplit(displayMessage, ' ', message); message = substituteEmotesInMessage(message, relevantEmotes); @@ -523,7 +552,9 @@ displayName = userGlobalDisplayName; } - disposeOfMessage({ displayName, message, color, subscriber, turbo, mod, isAction, userBadges, false, "" }); + bool isChannelMessage = isWhisper; + QString systemMessage = isWhisper ? ("Whispered to " + recipient + ":") : ""; + disposeOfMessage({ displayName, message, color, subscriber, turbo, mod, isAction, userBadges, isChannelMessage, systemMessage, false }); } } @@ -552,7 +583,7 @@ logged_in = true; //Join room automatically, if given - if (!room.isEmpty()) + if (!room.isEmpty() && !replayMode) join(room, roomChannelId); } @@ -604,7 +635,7 @@ 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); + emit messageReceived(m.name, m.messageList, m.color, m.subscriber, m.turbo, m.mod, m.isAction, m.badges, m.isChannelNotice, m.systemMessage, m.isWhisper); } else { // queue message to be shown when downloads are complete @@ -738,6 +769,7 @@ chatMessage.isAction = false; chatMessage.isChannelNotice = false; + chatMessage.isWhisper = false; foreach(const QString & tagStr, commandParse.tags) { Tag tag(tagStr); @@ -876,9 +908,35 @@ disposeOfMessage(parse.chatMessage); return; } + if (cmd.contains("WHISPER")) { + // Structure of message: + // @badges=;color=;display-name=TWitch_UserName;emotes=;message-id=2;thread-id=56781234_142000000;turbo=0;user-id=123456789;user-type= :twitch_username!twitch_username@twitch_username.tmi.twitch.tv WHISPER other_twitch_user :hi + CommandParse parse; + + parseMessageCommand(cmd, "WHISPER", parse); + + if (parse.wrongChannel) { + return; + } + + parse.chatMessage.isChannelNotice = true; + parse.chatMessage.isWhisper = true; + + parse.chatMessage.systemMessage = QString("Whisper from"); + + createEmoteMessageList(parseEmotesTag(parse.emotesStr), parse.chatMessage.messageList, parse.message); + + //qDebug() << "messageList " << messageList; + + qDebug() << "whisper isWhisper" << parse.chatMessage.isWhisper; + disposeOfMessage(parse.chatMessage); + return; + + } if(cmd.contains("NOTICE")) { QString text = cmd.remove(0, cmd.indexOf(':', cmd.indexOf("NOTICE")) + 1); emit noticeReceived(text); + return; } if(cmd.contains("GLOBALUSERSTATE")) { // Structure of message: @badges=turbo/1;color=#4100CC;display-name=user_name;emote-sets=0,1,22,345;user-id=12345678;user-type= :tmi.twitch.tv GLOBALUSERSTATE @@ -962,7 +1020,7 @@ } return; } - //qDebug() << "Unrecognized chat command:" << cmd; + qDebug() << "Unrecognized chat command:" << cmd; } QString IrcChat::getParamValue(QString params, QString param) { @@ -982,12 +1040,12 @@ //qDebug() << "Download queue complete; posting pending messages"; while (!msgQueue.empty()) { ChatMessage tmpMsg = msgQueue.first(); - emit messageReceived(tmpMsg.name, tmpMsg.messageList, tmpMsg.color, tmpMsg.subscriber, tmpMsg.turbo, tmpMsg.mod, tmpMsg.isAction, tmpMsg.badges, tmpMsg.isChannelNotice, tmpMsg.systemMessage); + emit messageReceived(tmpMsg.name, tmpMsg.messageList, tmpMsg.color, tmpMsg.subscriber, tmpMsg.turbo, tmpMsg.mod, tmpMsg.isAction, tmpMsg.badges, tmpMsg.isChannelNotice, tmpMsg.systemMessage, tmpMsg.isWhisper); msgQueue.pop_front(); } } } -bool IrcChat::bulkDownloadEmotes(QList<QString> keys) { - return _emoteProvider.bulkDownload(keys); +void IrcChat::bulkDownloadEmotes(QList<QString> keys) { + _emoteProvider.bulkDownload(keys); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/model/ircchat.h new/orion-1.5.1+git~20170503/src/model/ircchat.h --- old/orion-1.5.1+git~20170428/src/model/ircchat.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/model/ircchat.h 2017-05-03 21:01:12.000000000 +0200 @@ -50,6 +50,7 @@ QVariantList badges; bool isChannelNotice; QString systemMessage; + bool isWhisper; }; // Backend for chat @@ -106,19 +107,21 @@ void connectedChanged(); void emoteSetIDsChanged(); void anonymousChanged(); - void messageReceived(QString user, QVariantList message, QString chatColor, bool subscriber, bool turbo, bool mod, bool isAction, QVariantList badges, bool isChannelNotice, QString systemMessage); + void messageReceived(QString user, QVariantList message, QString chatColor, bool subscriber, bool turbo, bool mod, bool isAction, QVariantList badges, bool isChannelNotice, QString systemMessage, bool isWhisper); void noticeReceived(QString message); void myBadgesForChannel(QString channel, QList<QPair<QString, QString>> badges); void downloadComplete(); bool downloadError(); + + void bulkDownloadComplete(); public slots: void sendMessage(const QString &msg, const QVariantMap &relevantEmotes); void onSockStateChanged(); void login(); - bool bulkDownloadEmotes(QList<QString> keys); + void bulkDownloadEmotes(QList<QString> keys); private slots: void receive(); @@ -162,6 +165,7 @@ QTcpSocket *sock; QString room; QString roomChannelId; + bool replayMode; // map of channel name -> list of pairs (badge name, badge version) QMap<QString, QList<QPair<QString, QString>>> badgesByChannel; bool logged_in; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/network/networkmanager.cpp new/orion-1.5.1+git~20170503/src/network/networkmanager.cpp --- old/orion-1.5.1+git~20170428/src/network/networkmanager.cpp 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/network/networkmanager.cpp 2017-05-03 21:01:12.000000000 +0200 @@ -380,6 +380,38 @@ } +void NetworkManager::loadChatterList(const QString channel) { + qDebug() << "Loading viewer list for" << channel; + const QString url = QString(TWITCH_TMI_USER_API) + channel + QString("/chatters"); + + qDebug() << "Request" << url; + + QNetworkRequest request; + request.setUrl(url); + + QNetworkReply *reply = operation->get(request); + + connect(reply, SIGNAL(finished()), this, SLOT(chatterListReply())); +} + +void NetworkManager::chatterListReply() { + QNetworkReply* reply = qobject_cast<QNetworkReply *>(sender()); + + if (!handleNetworkError(reply)) { + return; + } + + QByteArray data = reply->readAll(); + + //qDebug() << data; + + QMap<QString, QList<QString>> ret = JsonParser::parseChatterList(data); + + emit chatterListLoadOperationFinished(ret); + + reply->deleteLater(); +} + void NetworkManager::getVodChatPiece(quint64 vodId, quint64 offset) { QString url = QString(TWITCH_RECHAT_API) + QString("?start=%1&video_id=v%2").arg(offset).arg(vodId); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/network/networkmanager.h new/orion-1.5.1+git~20170503/src/network/networkmanager.h --- old/orion-1.5.1+git~20170428/src/network/networkmanager.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/network/networkmanager.h 2017-05-03 21:01:12.000000000 +0200 @@ -81,6 +81,7 @@ Q_INVOKABLE void getVodChatPiece(quint64 vodId, quint64 offset); Q_INVOKABLE void cancelLastVodChatRequest(); Q_INVOKABLE void resetVodChat(); + Q_INVOKABLE void loadChatterList(const QString channel); QNetworkAccessManager *getManager() const; @@ -115,6 +116,7 @@ void vodStartGetOperationFinished(double); void vodChatPieceGetOperationFinished(QList<ReplayChatMessage>); + void chatterListLoadOperationFinished(QMap<QString, QList<QString>>); void networkAccessChanged(bool up); @@ -137,6 +139,7 @@ void streamReply(); void vodStartReply(); void vodChatPieceReply(); + void chatterListReply(); //Oauth slots void userReply(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/network/urls.h new/orion-1.5.1+git~20170503/src/network/urls.h --- old/orion-1.5.1+git~20170428/src/network/urls.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/network/urls.h 2017-05-03 21:01:12.000000000 +0200 @@ -19,6 +19,7 @@ #define TWITCH_API "https://api.twitch.tv/api" #define TWITCH_API_BASE "https://api.twitch.tv/kraken/base" #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 CLIENT_ID "0dpzlnp1w2bjlim3ldp0u96o4dq2gm" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/qml/PlayerView.qml new/orion-1.5.1+git~20170503/src/qml/PlayerView.qml --- old/orion-1.5.1+git~20170428/src/qml/PlayerView.qml 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/qml/PlayerView.qml 2017-05-03 21:01:12.000000000 +0200 @@ -519,7 +519,6 @@ top: parent.top bottom: parent.bottom right: parent.right - rightMargin: dp(5) } width: dp(50) height: width @@ -819,11 +818,33 @@ ChatView { id: chatview + // Use JS so we can control the order the anchors are set. + // https://doc.qt.io/qt-5/qtquick-positioning-anchors.html#changing-anchors anchors { top: parent.top bottom: parent.bottom - left: g_cman.swapChat ? parent.left : undefined - right: !g_cman.swapChat ? parent.right : undefined + } + + function updateAnchors() { + console.log("updateAnchors: g_cman.swapChat", g_cman.swapChat); + if (g_cman.swapChat) { + anchors.right = undefined; + anchors.left = parent.left; + } else { + anchors.left = undefined; + anchors.right = parent.right; + } + } + + Component.onCompleted: { + chatview.updateAnchors(); + } + + Connections { + target: g_cman + onSwapChatChanged: { + chatview.updateAnchors(); + } } width: visible && !smallMode ? dp(250) : 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/qml/fonts/fontAwesome.js new/orion-1.5.1+git~20170503/src/qml/fonts/fontAwesome.js --- old/orion-1.5.1+git~20170428/src/qml/fonts/fontAwesome.js 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/qml/fonts/fontAwesome.js 2017-05-03 21:01:12.000000000 +0200 @@ -28,6 +28,9 @@ "chat": "\uf086", "reload": "\uf021", "minimode": "\uf2d2", - "smile": "\uf1e8" + "smile": "\uf1e8", + "list-ul": "\uf0ca", + "list": "\uf03a", + "times": "\uf00d" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/qml/irc/Chat.qml new/orion-1.5.1+git~20170503/src/qml/irc/Chat.qml --- old/orion-1.5.1+git~20170428/src/qml/irc/Chat.qml 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/qml/irc/Chat.qml 2017-05-03 21:01:12.000000000 +0200 @@ -20,12 +20,12 @@ Item { id: root - signal messageReceived(string user, variant message, string chatColor, bool subscriber, bool turbo, bool isAction, var badges, bool isChannelNotice, string systemMessage) + signal messageReceived(string user, variant message, string chatColor, bool subscriber, bool turbo, bool isAction, var badges, bool isChannelNotice, string systemMessage, bool isWhisper) signal setEmotePath(string value) signal notify(string message) signal clear() signal emoteSetIDsChanged(var emoteSetIDs) - signal downloadComplete() + signal bulkDownloadComplete() signal channelBadgeUrlsLoaded(string channel, var badgeUrls) signal channelBadgeBetaUrlsLoaded(string channel, var badgeSetData) @@ -70,7 +70,7 @@ chat.replayStop(); } root.replayMode = false; - messageReceived("notice", null, "", false, false, false, [], true, "Joined channel #" + channelName) + messageReceived("notice", null, "", false, false, false, [], true, "Joined channel #" + channelName, false) g_cman.loadChannelBadgeUrls(channelName); g_cman.loadChannelBetaBadgeUrls(channelId); } @@ -80,7 +80,7 @@ root.channel = channelName root.channelId = channelId root.replayMode = true - messageReceived("notice", null, "", false, false, false, [], true, "Starting chat replay #" + channelName + " v" + vodId) + messageReceived("notice", null, "", false, false, false, [], true, "Starting chat replay #" + channelName + " v" + vodId, false) g_cman.loadChannelBadgeUrls(channelName); g_cman.loadChannelBetaBadgeUrls(channelId); } @@ -97,7 +97,7 @@ } function replaySeek(newOffset) { - messageReceived("notice", null, "", false, false, false, [], true, "Seeking to " + durationStr(newOffset)); + messageReceived("notice", null, "", false, false, false, [], true, "Seeking to " + durationStr(newOffset), false); chat.replaySeek(newOffset); } @@ -132,9 +132,13 @@ onConnectedChanged: { if (connected) { - console.log("Connected to chat") if (root.channel) { - joinChannel(root.channel, root.channelId) + if (root.replayMode) { + console.log("Reconnected; chat replay may resume") + } else { + console.log("Connected to chat") + joinChannel(root.channel, root.channelId) + } } } else { console.log("Disconnected from chat") @@ -143,21 +147,20 @@ onMessageReceived: { root.setEmotePath(emoteDirPath) - root.messageReceived(user, message, chatColor, subscriber, turbo, isAction, badges, isChannelNotice, systemMessage) + root.messageReceived(user, message, chatColor, subscriber, turbo, isAction, badges, isChannelNotice, systemMessage, isWhisper) } onNoticeReceived: { console.log("Notification received", message); - root.messageReceived("channel", [], null, null, false, false, {}, true, message) + root.messageReceived("channel", [], null, null, false, false, {}, true, message, false) } onEmoteSetIDsChanged: { root.emoteSetIDsChanged(emoteSetIDs) } - onDownloadComplete: { - console.log("inner download complete"); - root.downloadComplete(); + onBulkDownloadComplete: { + root.bulkDownloadComplete(); } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/qml/irc/ChatMessage.qml new/orion-1.5.1+git~20170503/src/qml/irc/ChatMessage.qml --- old/orion-1.5.1+git~20170428/src/qml/irc/ChatMessage.qml 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/qml/irc/ChatMessage.qml 2017-05-03 21:01:12.000000000 +0200 @@ -26,6 +26,7 @@ property string jsonBadgeEntries property string emoteDirPath property bool isChannelNotice + property bool isWhisper property string systemMessage property int fontSize: Styles.titleFont.smaller property var pmsg: JSON.parse(msg) @@ -252,13 +253,18 @@ { if (link.substr(0,5) === "user:") { - var value = "@"+link.replace('user:',"")+', ' - if (_input.text === "") - { - _input.text = value - } - else { - _input.text = _input.text + ' '+ value + var clickedUser = link.replace('user:',""); + if (isWhisper) { + _input.text = "/w " + clickedUser + " " + _input.text; + } else { + var value = "@" + clickedUser + ', '; + if (_input.text === "") + { + _input.text = value + } + else { + _input.text = _input.text + ' '+ value + } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/qml/irc/ChatView.qml new/orion-1.5.1+git~20170503/src/qml/irc/ChatView.qml --- old/orion-1.5.1+git~20170428/src/qml/irc/ChatView.qml 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/qml/irc/ChatView.qml 2017-05-03 21:01:12.000000000 +0200 @@ -15,6 +15,7 @@ import QtQuick 2.0 import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 +import QtQuick.Controls 2.0 import "../fonts/fontAwesome.js" as FontAwesome import "../styles.js" as Styles import "../components" @@ -33,11 +34,15 @@ status = 0 } + property bool chatViewVisible: root.width > 0 + visible: status > 0 property real _opacity: root.status > 1 ? 0.6 : 1.0 property int chatWidth: width + property bool viewerListEnabled: false + Rectangle { anchors.fill: parent color: Styles.sidebarBg @@ -54,6 +59,7 @@ function joinChannel(channel, channelId) { if (channel !== chat.channel || chat.replayMode) { + viewerListEnabled = false; chatModel.clear() chat.joinChannel(channel, channelId) } @@ -65,6 +71,7 @@ } function replayChat(channelName, channelId, vodId, startEpochTime) { + viewerListEnabled = false; chatModel.clear() chat.leaveChannel() chat.replayChat(channelName, channelId, vodId, startEpochTime); @@ -145,9 +152,198 @@ } } + + Item { + id: chatControls + anchors { + top: parent.top + right: parent.right + left: parent.left + } + height: dp(40) + + IconButton { + id: _viewerListButton + icon: viewerListEnabled ? "times" : "list" + + enabled: (!isVod && currentChannel && currentChannel.name) ? true : false + + anchors { + top: parent.top + right: parent.right + rightMargin: 5 + bottom: parent.bottom + } + width: height + + onClicked: { + viewerListEnabled = !viewerListEnabled + if (viewerListEnabled && (status == 0)) { + status++; + } + } + + ToolTip { + visible: _viewerListButton.mouseArea.containsMouse + delay: 666 + text: "Viewer List" + } + } + } + + Item { + id: chatContainer + + anchors { + top: chatControls.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + + Rectangle { + id: viewerList + enabled: viewerListEnabled + property bool loading: true + + height: enabled? parent.height : 0 + + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + z: 10 + + color: Styles.sidebarBg + opacity: root._opacity + + onEnabledChanged: { + if (enabled) { + viewerList.loading = true; + viewerListModel.clear(); + g_cman.loadChatterList(chat.channel); + } + } + + SpinnerIcon { + id: spinner + anchors.centerIn: parent + iconSize: parent.width * 0.1 + visible: viewerList.loading && viewerList.enabled + } + + Item { + id: viewerListHeading + visible: viewerList.enabled + anchors { + bottom: parent.top + left: parent.left + right: parent.right + } + + height: dp(40) + + Label { + anchors.centerIn: parent + + text: "Viewer List" + color: Styles.textColor + font.pixelSize: Styles.titleFont.bigger + font.bold: true + } + } + + ListView { + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + top: viewerListHeading.bottom + } + + model: ListModel { + id: viewerListModel + } + + Connections { + target: g_cman + onChatterListLoaded: { + viewerList.loading = false; + + var groupOrder = ["staff", "global_mods", "admins", "moderators", "viewers"]; + + for (var j = 0; j < groupOrder.length; j++) { + var groupName = groupOrder[j]; + var group = chatters[groupName]; + if (!group) { + continue; + } + + for (var i = 0; i < group.length; i++) { + var chatter = group[i]; + viewerListModel.append({"groupName": groupName, "user": chatter}); + } + } + } + } + + clip: true + delegate: Item { + height: dp(25) + Text { + text: user + color: Styles.textColor + anchors { + fill: parent + leftMargin: dp(5) + rightMargin: dp(5) + } + font.capitalization: Font.Capitalize + } + } + + section { + property: "groupName" + criteria: ViewSection.FullString + delegate: Item { + height: dp(50) + Text { + anchors { + leftMargin: dp(5) + rightMargin: dp(5) + bottomMargin: dp(5) + left: parent.left + right: parent.right + bottom: parent.bottom + } + + font.capitalization: Font.AllUppercase + text: section + //color: Styles.textColor + color: Styles.purple + font.pixelSize: Styles.titleFont.smaller + } + } + + } + + } + } + ListView { id: list + visible: !viewerList.enabled && root.chatViewVisible + property bool lock: true property int scrollbuf: 0 property int previousY: 0 @@ -179,6 +375,7 @@ emoteDirPath: chat.emoteDirPath isChannelNotice: model.isChannelNotice systemMessage: model.systemMessage + isWhisper: model.isWhisper highlightOpacity: root._opacity anchors { @@ -291,7 +488,7 @@ right: parent.right } - visible: !chat.isAnonymous && !chat.replayMode + visible: !chat.isAnonymous && !chat.replayMode && !viewerList.enabled Rectangle { anchors.fill: parent @@ -344,7 +541,7 @@ property bool pickerLoaded: false - visible: root.width > 0 + visible: root.chatViewVisible width: height @@ -561,12 +758,7 @@ } curDownloading ++; console.log("Downloading emote set #", curDownloading, curSetID); - var waitForDownload = chat.bulkDownloadEmotes(curSetList); - - if (!waitForDownload) { - showLastSet(); - nextDownload(); - } + chat.bulkDownloadEmotes(curSetList); } else { emotePickerDownloadsInProgress = false; _emotePicker.loading = false; @@ -589,7 +781,7 @@ Connections { target: chat - onDownloadComplete: { + onBulkDownloadComplete: { //console.log("outer download complete"); if (_emoteButton.emotePickerDownloadsInProgress) { //console.log("handling emote picker set finished"); @@ -620,6 +812,8 @@ property var globalBetaBadgeSetData: {} property var lastBetaBadgeSetData: {} + property bool debugOutput: false + onLastEmoteSetsChanged: { initEmotesMaps(); } @@ -699,7 +893,7 @@ } onMessageReceived: { - //console.log("ChatView chat override onMessageReceived; typeof message " + typeof(message) + " toString: " + message.toString()) + if (debugOutput) console.log("ChatView chat override onMessageReceived; typeof message " + typeof(message) + " toString: " + message.toString()); if (chatColor != "") { colors[user] = chatColor; @@ -711,17 +905,17 @@ // ListElement doesn't support putting in an array value, ugh. var serializedMessage = JSON.stringify(message); - console.log("onMessageReceived: passing: " + serializedMessage); + if (debugOutput) console.log("onMessageReceived: passing: " + serializedMessage); var badgeEntries = []; var imageFormatToUse = "image"; var badgesSeen = {}; - console.log("badges for this message:") + if (debugOutput) console.log("badges for this message:") for (var k = 0; k < badges.length; k++) { var badgeName = badges[k][0]; var versionStr = badges[k][1]; - console.log(" badge", badgeName, versionStr); + if (debugOutput) console.log(" badge", badgeName, versionStr); if (badgesSeen[badgeName]) { continue; @@ -741,7 +935,7 @@ console.log(" available versions are", keysStr(badgeSetData)) } else { var entry = {"name": versionObj.title, "url": badgeLocalUrl, "click_action": versionObj.click_action, "click_url": versionObj.click_url} - console.log("adding entry", JSON.stringify(entry)); + if (debugOutput) console.log("adding entry", JSON.stringify(entry)); badgeEntries.push(entry); curBadgeAdded = true; } @@ -749,12 +943,14 @@ var badgeUrls = lastBadgeUrls[badgeName]; if (!curBadgeAdded && badgeUrls != null) { - console.log(" badge urls:") - for (var j in badgeUrls) { - console.log(" key", j, "value", badgeUrls[j]); + if (debugOutput) { + console.log(" badge urls:") + for (var j in badgeUrls) { + console.log(" key", j, "value", badgeUrls[j]); + } } var entry = {"name": badgeName, "url": badgeLocalUrl}; - console.log("adding entry", JSON.stringify(entry)); + if (debugOutput) console.log("adding entry", JSON.stringify(entry)); badgeEntries.push(entry); curBadgeAdded = true; } @@ -766,7 +962,7 @@ var jsonBadgeEntries = JSON.stringify(badgeEntries); - chatModel.append({"user": user, "message": serializedMessage, "isAction": isAction, "jsonBadgeEntries": jsonBadgeEntries, "isChannelNotice": isChannelNotice, "systemMessage": systemMessage}) + chatModel.append({"user": user, "message": serializedMessage, "isAction": isAction, "jsonBadgeEntries": jsonBadgeEntries, "isChannelNotice": isChannelNotice, "systemMessage": systemMessage, "isWhisper": isWhisper}) list.scrollbuf = 6 } @@ -824,3 +1020,4 @@ } } } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/util/jsonparser.cpp new/orion-1.5.1+git~20170503/src/util/jsonparser.cpp --- old/orion-1.5.1+git~20170428/src/util/jsonparser.cpp 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/util/jsonparser.cpp 2017-05-03 21:01:12.000000000 +0200 @@ -610,3 +610,29 @@ return out; } + +QMap<QString, QList<QString>> JsonParser::parseChatterList(const QByteArray &data) +{ + QMap<QString, QList<QString>> out; + + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + + if (error.error == QJsonParseError::NoError) { + QJsonObject json = doc.object(); + + QJsonObject chatters = json["chatters"].toObject(); + + for (auto groupEntry = chatters.constBegin(); groupEntry != chatters.constEnd(); groupEntry++) { + QList<QString> groupChatters; + const QJsonArray & groupChattersJson = groupEntry.value().toArray(); + for (const auto & chatter : groupChattersJson) { + groupChatters.append(chatter.toString()); + } + out.insert(groupEntry.key(), groupChatters); + } + } + + return out; + +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/orion-1.5.1+git~20170428/src/util/jsonparser.h new/orion-1.5.1+git~20170503/src/util/jsonparser.h --- old/orion-1.5.1+git~20170428/src/util/jsonparser.h 2017-04-28 15:15:59.000000000 +0200 +++ new/orion-1.5.1+git~20170503/src/util/jsonparser.h 2017-05-03 21:01:12.000000000 +0200 @@ -54,6 +54,7 @@ static QMap<QString, QMap<QString, QString>> parseChannelBadgeUrls(const QByteArray &data); 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); }; #endif // JSONPARSER_H
