diff --git a/.CI/CreateDMG.sh b/.CI/CreateDMG.sh index 60a8f534..8ad191e2 100755 --- a/.CI/CreateDMG.sh +++ b/.CI/CreateDMG.sh @@ -1,13 +1,13 @@ #!/bin/sh echo "Running MACDEPLOYQT" -/usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg -echo "Creating APP folder" -mkdir app -echo "Running hdiutil attach on the built DMG" -hdiutil attach chatterino.dmg -echo "Copying chatterino.app into the app folder" -cp -r /Volumes/chatterino/chatterino.app app/ -echo "Creating DMG with create-dmg" -create-dmg --volname Chatterino2 --volicon ../resources/chatterino.icns --icon-size 50 --app-drop-link 0 0 --format UDBZ chatterino-osx.dmg app/ -echo "DONE" +/usr/local/opt/qt/bin/macdeployqt chatterino.app +echo "Creating python3 virtual environment" +python3 -m venv venv +echo "Entering python3 virtual environment" +. venv/bin/activate +echo "Installing dmgbuild" +python3 -m pip install dmgbuild +echo "Running dmgbuild.." +dmgbuild --settings ./../.CI/dmg-settings.py -D app=./chatterino.app Chatterino2 chatterino-osx.dmg +echo "Done!" diff --git a/.CI/dmg-settings.py b/.CI/dmg-settings.py new file mode 100644 index 00000000..6b068fa1 --- /dev/null +++ b/.CI/dmg-settings.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import biplist +import os.path + +# +# Example settings file for dmgbuild +# + +# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg + +# You can actually use this file for your own application (not just TextEdit) +# by doing e.g. +# +# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg + +# .. Useful stuff .............................................................. + +application = defines.get('app', '/Applications/TextEdit.app') +appname = os.path.basename(application) + +def icon_from_app(app_path): + plist_path = os.path.join(app_path, 'Contents', 'Info.plist') + plist = biplist.readPlist(plist_path) + icon_name = plist['CFBundleIconFile'] + icon_root,icon_ext = os.path.splitext(icon_name) + if not icon_ext: + icon_ext = '.icns' + icon_name = icon_root + icon_ext + return os.path.join(app_path, 'Contents', 'Resources', icon_name) + +# .. Basics .................................................................... + +# Uncomment to override the output filename +# filename = 'test.dmg' + +# Uncomment to override the output volume name +# volume_name = 'Test' + +# Volume format (see hdiutil create -help) +format = defines.get('format', 'UDBZ') + +# Volume size +size = defines.get('size', None) + +# Files to include +files = [ application ] + +# Symlinks to create +symlinks = { 'Applications': '/Applications' } + +# Volume icon +# +# You can either define icon, in which case that icon file will be copied to the +# image, *or* you can define badge_icon, in which case the icon file you specify +# will be used to badge the system's Removable Disk icon +# +#icon = '/path/to/icon.icns' +badge_icon = icon_from_app(application) + +# Where to put the icons +icon_locations = { + appname: (140, 120), + 'Applications': (500, 120) + } + +# .. Window configuration ...................................................... + +# Background +# +# This is a STRING containing any of the following: +# +# #3344ff - web-style RGB color +# #34f - web-style RGB color, short form (#34f == #3344ff) +# rgb(1,0,0) - RGB color, each value is between 0 and 1 +# hsl(120,1,.5) - HSL (hue saturation lightness) color +# hwb(300,0,0) - HWB (hue whiteness blackness) color +# cmyk(0,1,0,0) - CMYK color +# goldenrod - X11/SVG named color +# builtin-arrow - A simple built-in background with a blue arrow +# /foo/bar/baz.png - The path to an image file +# +# The hue component in hsl() and hwb() may include a unit; it defaults to +# degrees ('deg'), but also supports radians ('rad') and gradians ('grad' +# or 'gon'). +# +# Other color components may be expressed either in the range 0 to 1, or +# as percentages (e.g. 60% is equivalent to 0.6). +background = 'builtin-arrow' + +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 180 + +# Window position in ((x, y), (w, h)) format +window_rect = ((100, 100), (640, 280)) + +# Select the default view; must be one of +# +# 'icon-view' +# 'list-view' +# 'column-view' +# 'coverflow' +# +default_view = 'icon-view' + +# General view configuration +show_icon_preview = False + +# Set these to True to force inclusion of icon/list view settings (otherwise +# we only include settings for the default view) +include_icon_view_settings = 'auto' +include_list_view_settings = 'auto' + +# .. Icon view configuration ................................................... + +arrange_by = None +grid_offset = (0, 0) +grid_spacing = 100 +scroll_position = (0, 0) +label_pos = 'bottom' # or 'right' +text_size = 16 +icon_size = 128 + +# .. List view configuration ................................................... + +# Column names are as follows: +# +# name +# date-modified +# date-created +# date-added +# date-last-opened +# size +# kind +# label +# version +# comments +# +list_icon_size = 16 +list_text_size = 12 +list_scroll_position = (0, 0) +list_sort_by = 'name' +list_use_relative_dates = True +list_calculate_all_sizes = False, +list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added') +list_column_widths = { + 'name': 300, + 'date-modified': 181, + 'date-created': 181, + 'date-added': 181, + 'date-last-opened': 181, + 'size': 97, + 'kind': 115, + 'label': 100, + 'version': 75, + 'comments': 300, + } +list_column_sort_directions = { + 'name': 'ascending', + 'date-modified': 'descending', + 'date-created': 'descending', + 'date-added': 'descending', + 'date-last-opened': 'descending', + 'size': 'descending', + 'kind': 'ascending', + 'label': 'ascending', + 'version': 'ascending', + 'comments': 'ascending', + } + +# .. License configuration ..................................................... + +# Text in the license configuration is stored in the resources, which means +# it gets stored in a legacy Mac encoding according to the language. dmgbuild +# will *try* to convert Unicode strings to the appropriate encoding, *but* +# you should be aware that Python doesn't support all of the necessary encodings; +# in many cases you will need to encode the text yourself and use byte strings +# instead here. + +# Recognized language names are: +# +# af_ZA, ar, be_BY, bg_BG, bn, bo, br, ca_ES, cs_CZ, cy, da_DK, de_AT, de_CH, +# de_DE, dz_BT, el_CY, el_GR, en_AU, en_CA, en_GB, en_IE, en_SG, en_US, eo, +# es_419, es_ES, et_EE, fa_IR, fi_FI, fo_FO, fr_001, fr_BE, fr_CA, fr_CH, +# fr_FR, ga-Latg_IE, ga_IE, gd, grc, gu_IN, gv, he_IL, hi_IN, hr_HR, hu_HU, +# hy_AM, is_IS, it_CH, it_IT, iu_CA, ja_JP, ka_GE, kl, ko_KR, lt_LT, lv_LV, +# mk_MK, mr_IN, mt_MT, nb_NO, ne_NP, nl_BE, nl_NL, nn_NO, pa, pl_PL, pt_BR, +# pt_PT, ro_RO, ru_RU, se, sk_SK, sl_SI, sr_RS, sv_SE, th_TH, to_TO, tr_TR, +# uk_UA, ur_IN, ur_PK, uz_UZ, vi_VN, zh_CN, zh_TW + +# license = { +# 'default-language': 'en_US', +# 'licenses': { +# # For each language, the text of the license. This can be plain text, +# # RTF (in which case it must start "{\rtf1"), or a path to a file +# # containing the license text. If you're using RTF, +# # watch out for Python escaping (or read it from a file). +# 'English': b'''{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820 +# {\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;} +# {\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;} +# {\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;} +# \\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133 +# \\deftab720 +# \\pard\\pardeftab720\\sa160\\partightenfactor0 + +# \\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0 +# \\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\ +# \\pard\\pardeftab720\\sa160\\partightenfactor0 + +# \\fs36 \\cf2 \\strokec2 What is this?\\ +# \\pard\\pardeftab720\\sa160\\partightenfactor0 + +# \\f1\\b0\\fs22 \\cf2 \\strokec2 This is the English license. It says what you are allowed to do with this software.\\ +# \\ +# }''', +# }, +# 'buttons': { +# # For each language, text for the buttons on the licensing window. +# # +# # Default buttons and text are built-in for the following languages: +# # +# # English (en_US), German (de_DE), Spanish (es_ES), French (fr_FR), +# # Italian (it_IT), Japanese (ja_JP), Dutch (nl_NL), Swedish (sv_SE), +# # Brazilian Portuguese (pt_BR), Simplified Chinese (zh_CN), +# # Traditional Chinese (zh_TW), Danish (da_DK), Finnish (fi_FI), +# # Korean (ko_KR), Norwegian (nb_NO) +# # +# # You don't need to specify them for those languages; if you fail to +# # specify them for some other language, English will be used instead. + +# 'en_US': ( +# b'English', +# b'Agree', +# b'Disagree', +# b'Print', +# b'Save', +# b'If you agree with the terms of this license, press "Agree" to ' +# b'install the software. If you do not agree, press "Disagree".' +# ), +# }, +# } + diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fd32834..75bdc29d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,9 +25,7 @@ jobs: submodules: true - name: Install Qt - uses: jurplel/install-qt-action@v2 - with: - modules: qtwebengine + uses: jurplel/install-qt-action@v1 # WINDOWS - name: Cache conan @@ -79,7 +77,7 @@ jobs: # LINUX - name: Install dependencies (Ubuntu) if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get update && sudo apt-get -y install libssl-dev libboost-dev libboost-system-dev libboost-filesystem-dev libpulse-dev libxkbcommon-x11-0 libgstreamer-plugins-base1.0-0 + run: sudo apt-get update && sudo apt-get -y install libssl-dev libboost-dev libboost-system-dev libboost-filesystem-dev libpulse-dev libxkbcommon-x11-0 libgstreamer-plugins-base1.0-0 build-essential libgl1-mesa-dev - name: Build (Ubuntu) if: startsWith(matrix.os, 'ubuntu') diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..477bf3f5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## Unversioned +- Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549) +- Bugfix: Fix preview on hover not working when Animated emotes options was disabled (#1546) +- Bugfix: FFZ custom mod badges no longer scale with the emote scale options (#1602) +- Settings open faster +- Dev: Fully remove Twitch Chatroom support diff --git a/.travis.yml b/_.travis.yml similarity index 100% rename from .travis.yml rename to _.travis.yml diff --git a/chatterino.pro b/chatterino.pro index 68058183..243a97ec 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -131,21 +131,16 @@ SOURCES += \ src/controllers/commands/CommandController.cpp \ src/controllers/commands/CommandModel.cpp \ src/controllers/highlights/HighlightBlacklistModel.cpp \ - src/controllers/highlights/HighlightController.cpp \ src/controllers/highlights/HighlightModel.cpp \ src/controllers/highlights/HighlightPhrase.cpp \ src/controllers/highlights/UserHighlightModel.cpp \ - src/controllers/ignores/IgnoreController.cpp \ src/controllers/ignores/IgnoreModel.cpp \ src/controllers/moderationactions/ModerationAction.cpp \ src/controllers/moderationactions/ModerationActionModel.cpp \ - src/controllers/moderationactions/ModerationActions.cpp \ src/controllers/notifications/NotificationController.cpp \ src/controllers/notifications/NotificationModel.cpp \ - src/controllers/pings/PingController.cpp \ - src/controllers/pings/PingModel.cpp \ + src/controllers/pings/MutedChannelModel.cpp \ src/controllers/taggedusers/TaggedUser.cpp \ - src/controllers/taggedusers/TaggedUsersController.cpp \ src/controllers/taggedusers/TaggedUsersModel.cpp \ src/debug/Benchmark.cpp \ src/main.cpp \ @@ -178,15 +173,14 @@ SOURCES += \ src/providers/irc/IrcConnection2.cpp \ src/providers/irc/IrcServer.cpp \ src/providers/LinkResolver.cpp \ - src/providers/twitch/ChatroomChannel.cpp \ + src/providers/twitch/api/Helix.cpp \ + src/providers/twitch/api/Kraken.cpp \ src/providers/twitch/IrcMessageHandler.cpp \ - src/providers/twitch/PartialTwitchUser.cpp \ src/providers/twitch/PubsubActions.cpp \ src/providers/twitch/PubsubClient.cpp \ src/providers/twitch/PubsubHelpers.cpp \ src/providers/twitch/TwitchAccount.cpp \ src/providers/twitch/TwitchAccountManager.cpp \ - src/providers/twitch/TwitchApi.cpp \ src/providers/twitch/TwitchBadge.cpp \ src/providers/twitch/TwitchBadges.cpp \ src/providers/twitch/TwitchChannel.cpp \ @@ -328,7 +322,6 @@ HEADERS += \ src/controllers/commands/CommandModel.hpp \ src/controllers/highlights/HighlightBlacklistModel.hpp \ src/controllers/highlights/HighlightBlacklistUser.hpp \ - src/controllers/highlights/HighlightController.hpp \ src/controllers/highlights/HighlightModel.hpp \ src/controllers/highlights/HighlightPhrase.hpp \ src/controllers/highlights/UserHighlightModel.hpp \ @@ -337,13 +330,10 @@ HEADERS += \ src/controllers/ignores/IgnorePhrase.hpp \ src/controllers/moderationactions/ModerationAction.hpp \ src/controllers/moderationactions/ModerationActionModel.hpp \ - src/controllers/moderationactions/ModerationActions.hpp \ src/controllers/notifications/NotificationController.hpp \ src/controllers/notifications/NotificationModel.hpp \ - src/controllers/pings/PingController.hpp \ - src/controllers/pings/PingModel.hpp \ + src/controllers/pings/MutedChannelModel.hpp \ src/controllers/taggedusers/TaggedUser.hpp \ - src/controllers/taggedusers/TaggedUsersController.hpp \ src/controllers/taggedusers/TaggedUsersModel.hpp \ src/debug/AssertInGuiThread.hpp \ src/debug/Benchmark.hpp \ @@ -383,16 +373,15 @@ HEADERS += \ src/providers/irc/IrcConnection2.hpp \ src/providers/irc/IrcServer.hpp \ src/providers/LinkResolver.hpp \ - src/providers/twitch/ChatroomChannel.hpp \ + src/providers/twitch/api/Helix.hpp \ + src/providers/twitch/api/Kraken.hpp \ src/providers/twitch/EmoteValue.hpp \ src/providers/twitch/IrcMessageHandler.hpp \ - src/providers/twitch/PartialTwitchUser.hpp \ src/providers/twitch/PubsubActions.hpp \ src/providers/twitch/PubsubClient.hpp \ src/providers/twitch/PubsubHelpers.hpp \ src/providers/twitch/TwitchAccount.hpp \ src/providers/twitch/TwitchAccountManager.hpp \ - src/providers/twitch/TwitchApi.hpp \ src/providers/twitch/TwitchBadge.hpp \ src/providers/twitch/TwitchBadges.hpp \ src/providers/twitch/TwitchChannel.hpp \ @@ -437,6 +426,7 @@ HEADERS += \ src/util/LayoutCreator.hpp \ src/util/LayoutHelper.hpp \ src/util/Overloaded.hpp \ + src/util/PersistSignalVector.hpp \ src/util/PostToThread.hpp \ src/util/QObjectRef.hpp \ src/util/QStringHash.hpp \ @@ -564,3 +554,11 @@ git_hash = $$str_member($$git_commit, 0, 8) DEFINES += CHATTERINO_GIT_COMMIT=\\\"$$git_commit\\\" DEFINES += CHATTERINO_GIT_RELEASE=\\\"$$git_release\\\" DEFINES += CHATTERINO_GIT_HASH=\\\"$$git_hash\\\" + +CONFIG(debug, debug|release) { + message("Building Chatterino2 DEBUG") +} else { + message("Building Chatterino2 RELEASE") +} + +message("Injected git values: $$git_commit ($$git_release) $$git_hash") diff --git a/resources/contributors.txt b/resources/contributors.txt index ea0644a7..cf94931e 100644 --- a/resources/contributors.txt +++ b/resources/contributors.txt @@ -29,6 +29,7 @@ TranRed | https://github.com/TranRed | | Contributor RAnders00 | https://github.com/RAnders00 | | Contributor YungLPR | https://github.com/leon-richardt | | Contributor Mm2PL | https://github.com/mm2pl | | Contributor +gempir | https://github.com/gempir | | Contributor # If you are a contributor add yourself above this line Defman21 | https://github.com/Defman21 | | Documentation diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss index 590dff5f..a3172fd6 100644 --- a/resources/qss/settings.qss +++ b/resources/qss/settings.qss @@ -39,7 +39,6 @@ chatterino--TitleLabel { font-family: "Segoe UI light"; font-size: 24px; color: #4FC3F7; - margin-top: 16px; } chatterino--DescriptionLabel { diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index dbb639bb..4fe4cf72 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -28,8 +28,9 @@ buttons/unmod.png buttons/update.png buttons/updateError.png - com.chatterino.chatterino.desktop chatterino.icns + com.chatterino.chatterino.appdata.xml + com.chatterino.chatterino.desktop contributors.txt emoji.json emojidata.txt @@ -50,6 +51,12 @@ licenses/websocketpp.txt pajaDank.png qss/settings.qss + scrolling/downScroll.png + scrolling/downScroll.svg + scrolling/neutralScroll.png + scrolling/neutralScroll.svg + scrolling/upScroll.png + scrolling/upScroll.svg settings/about.svg settings/aboutlogo.png settings/accounts.svg diff --git a/resources/scrolling/downScroll.png b/resources/scrolling/downScroll.png new file mode 100644 index 00000000..cc46bb5c Binary files /dev/null and b/resources/scrolling/downScroll.png differ diff --git a/resources/scrolling/downScroll.svg b/resources/scrolling/downScroll.svg new file mode 100644 index 00000000..d2e8992a --- /dev/null +++ b/resources/scrolling/downScroll.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/resources/scrolling/neutralScroll.png b/resources/scrolling/neutralScroll.png new file mode 100644 index 00000000..99ad1e7f Binary files /dev/null and b/resources/scrolling/neutralScroll.png differ diff --git a/resources/scrolling/neutralScroll.svg b/resources/scrolling/neutralScroll.svg new file mode 100644 index 00000000..6ad76c8c --- /dev/null +++ b/resources/scrolling/neutralScroll.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/resources/scrolling/upScroll.png b/resources/scrolling/upScroll.png new file mode 100644 index 00000000..43364297 Binary files /dev/null and b/resources/scrolling/upScroll.png differ diff --git a/resources/scrolling/upScroll.svg b/resources/scrolling/upScroll.svg new file mode 100644 index 00000000..d25c3f10 --- /dev/null +++ b/resources/scrolling/upScroll.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/src/Application.cpp b/src/Application.cpp index fe0aab34..6934ebbd 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -5,12 +5,8 @@ #include "common/Args.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" -#include "controllers/highlights/HighlightController.hpp" #include "controllers/ignores/IgnoreController.hpp" -#include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/notifications/NotificationController.hpp" -#include "controllers/pings/PingController.hpp" -#include "controllers/taggedusers/TaggedUsersController.hpp" #include "messages/MessageBuilder.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" @@ -54,16 +50,10 @@ Application::Application(Settings &_settings, Paths &_paths) , accounts(&this->emplace()) , commands(&this->emplace()) - , highlights(&this->emplace()) , notifications(&this->emplace()) - , pings(&this->emplace()) - , ignores(&this->emplace()) - , taggedUsers(&this->emplace()) - , moderationActions(&this->emplace()) , twitch2(&this->emplace()) , chatterinoBadges(&this->emplace()) , logging(&this->emplace()) - { this->instance = this; @@ -115,9 +105,6 @@ void Application::initialize(Settings &settings, Paths &paths) this->initNm(paths); this->initPubsub(); - - this->moderationActions->items.delayedItemsChanged.connect( - [this] { this->windows->forceLayoutChannelViews(); }); } int Application::run(QApplication &qtApp) @@ -130,6 +117,13 @@ int Application::run(QApplication &qtApp) getSettings()->betaUpdates.connect( [] { Updates::instance().checkForUpdates(); }, false); + getSettings()->moderationActions.delayedItemsChanged.connect( + [this] { this->windows->forceLayoutChannelViews(); }); + + getSettings()->highlightedMessages.delayedItemsChanged.connect( + [this] { this->windows->forceLayoutChannelViews(); }); + getSettings()->highlightedUsers.delayedItemsChanged.connect( + [this] { this->windows->forceLayoutChannelViews(); }); return qtApp.exec(); } @@ -156,14 +150,6 @@ void Application::initNm(Paths &paths) void Application::initPubsub() { - this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) { - qDebug() << "WHISPER SENT LOL"; // - }); - - this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) { - qDebug() << "WHISPER RECEIVED LOL"; // - }); - this->twitch.pubsub->signals_.moderation.chatCleared.connect( [this](const auto &action) { auto chan = diff --git a/src/Application.hpp b/src/Application.hpp index fac8ec0f..0c0e54bc 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -3,6 +3,7 @@ #include #include +#include "common/SignalVector.hpp" #include "common/Singleton.hpp" #include "singletons/NativeMessaging.hpp" @@ -12,13 +13,8 @@ class TwitchIrcServer; class PubSub; class CommandController; -class HighlightController; -class IgnoreController; -class TaggedUsersController; class AccountController; -class ModerationActions; class NotificationController; -class PingController; class Theme; class WindowManager; @@ -58,12 +54,7 @@ public: AccountController *const accounts{}; CommandController *const commands{}; - HighlightController *const highlights{}; NotificationController *const notifications{}; - PingController *const pings{}; - IgnoreController *const ignores{}; - TaggedUsersController *const taggedUsers{}; - ModerationActions *const moderationActions{}; TwitchIrcServer *const twitch2{}; ChatterinoBadges *const chatterinoBadges{}; diff --git a/src/BaseTheme.cpp b/src/BaseTheme.cpp index 99ddddb2..306f13f5 100644 --- a/src/BaseTheme.cpp +++ b/src/BaseTheme.cpp @@ -159,21 +159,6 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier) this->messages.backgrounds.regular = getColor(0, sat, 1); this->messages.backgrounds.alternate = getColor(0, sat, 0.96); - if (isLight_) - { - this->messages.backgrounds.highlighted = - blendColors(themeColor, this->messages.backgrounds.regular, 0.8); - } - else - { - // REMOVED - // this->messages.backgrounds.highlighted = - // QColor(getSettings()->highlightColor); - } - - this->messages.backgrounds.subscription = - blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7); - // this->messages.backgrounds.resub // this->messages.backgrounds.whisper this->messages.disabled = getColor(0, sat, 1, 0.6); diff --git a/src/BaseTheme.hpp b/src/BaseTheme.hpp index d9980813..36e14c07 100644 --- a/src/BaseTheme.hpp +++ b/src/BaseTheme.hpp @@ -64,8 +64,6 @@ public: struct { QColor regular; QColor alternate; - QColor highlighted; - QColor subscription; // QColor whisper; } backgrounds; diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 3088b6da..e025d7f2 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -154,11 +154,6 @@ namespace { toBeRemoved << info.absoluteFilePath(); } } - - for (auto &&path : toBeRemoved) - { - qDebug() << path << QFile(path).remove(); - } } } // namespace diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp index e7aab5d5..b0e5e6ce 100644 --- a/src/autogenerated/ResourcesAutogen.cpp +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -30,6 +30,9 @@ Resources2::Resources2() this->error = QPixmap(":/error.png"); this->icon = QPixmap(":/icon.png"); this->pajaDank = QPixmap(":/pajaDank.png"); + this->scrolling.downScroll = QPixmap(":/scrolling/downScroll.png"); + this->scrolling.neutralScroll = QPixmap(":/scrolling/neutralScroll.png"); + this->scrolling.upScroll = QPixmap(":/scrolling/upScroll.png"); this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png"); this->split.down = QPixmap(":/split/down.png"); this->split.left = QPixmap(":/split/left.png"); @@ -50,4 +53,4 @@ Resources2::Resources2() this->twitch.vip = QPixmap(":/twitch/vip.png"); } -} // namespace chatterino +} // namespace chatterino \ No newline at end of file diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp index cb342363..cbd6c0bf 100644 --- a/src/autogenerated/ResourcesAutogen.hpp +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -1,5 +1,4 @@ #include - #include "common/Singleton.hpp" namespace chatterino { @@ -39,6 +38,11 @@ public: QPixmap error; QPixmap icon; QPixmap pajaDank; + struct { + QPixmap downScroll; + QPixmap neutralScroll; + QPixmap upScroll; + } scrolling; struct { QPixmap aboutlogo; } settings; @@ -65,4 +69,4 @@ public: } twitch; }; -} // namespace chatterino +} // namespace chatterino \ No newline at end of file diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index 4866e8a2..f3d368da 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -38,13 +38,7 @@ bool CompletionModel::TaggedString::operator<(const TaggedString &that) const return this->isEmote(); } - // try comparing insensitively, if they are the same then senstively - // (fixes order of LuL and LUL) - int k = QString::compare(this->string, that.string, Qt::CaseInsensitive); - if (k == 0) - return this->string > that.string; - - return k < 0; + return CompletionModel::compareStrings(this->string, that.string); } // @@ -133,7 +127,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) TaggedString::Type::Username); } } - else + else if (!getSettings()->userCompletionOnlyWithAt) { for (const auto &name : usernames->subrange(Prefix(usernamePrefix))) @@ -191,4 +185,15 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord) } } +bool CompletionModel::compareStrings(const QString &a, const QString &b) +{ + // try comparing insensitively, if they are the same then senstively + // (fixes order of LuL and LUL) + int k = QString::compare(a, b, Qt::CaseInsensitive); + if (k == 0) + return a > b; + + return k < 0; +} + } // namespace chatterino diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 321929e7..2ce37439 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -49,6 +49,8 @@ public: void refresh(const QString &prefix, bool isFirstWord = false); + static bool compareStrings(const QString &a, const QString &b); + private: TaggedString createUser(const QString &str); diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp index 2995cbba..33bd8254 100644 --- a/src/common/SignalVector.hpp +++ b/src/common/SignalVector.hpp @@ -4,244 +4,171 @@ #include #include #include -#include #include #include "debug/AssertInGuiThread.hpp" namespace chatterino { -template -struct SignalVectorItemArgs { - const TVectorItem &item; +template +struct SignalVectorItemEvent { + const T &item; int index; void *caller; }; -template -class ReadOnlySignalVector : boost::noncopyable +template +class SignalVector : boost::noncopyable { - using VecIt = typename std::vector::iterator; - public: - struct Iterator - : public std::iterator { - Iterator(VecIt &&it, std::shared_mutex &mutex) - : it_(std::move(it)) - , lock_(mutex) - , mutex_(mutex) - { - } + pajlada::Signals::Signal> itemInserted; + pajlada::Signals::Signal> itemRemoved; + pajlada::Signals::NoArgSignal delayedItemsChanged; - Iterator(const Iterator &other) - : it_(other.it_) - , lock_(other.mutex_) - , mutex_(other.mutex_) - { - } - - Iterator &operator=(const Iterator &other) - { - this->lock_ = std::shared_lock(other.mutex_.get()); - this->mutex_ = other.mutex_; - - return *this; - } - - TVectorItem &operator*() - { - return it_.operator*(); - } - - Iterator &operator++() - { - ++this->it_; - return *this; - } - - bool operator==(const Iterator &other) - { - return this->it_ == other.it_; - } - - bool operator!=(const Iterator &other) - { - return this->it_ != other.it_; - } - - auto operator-(const Iterator &other) - { - return this->it_ - other.it_; - } - - private: - VecIt it_; - std::shared_lock lock_; - std::reference_wrapper mutex_; - }; - - ReadOnlySignalVector() + SignalVector() + : readOnly_(new std::vector()) { QObject::connect(&this->itemsChangedTimer_, &QTimer::timeout, [this] { this->delayedItemsChanged.invoke(); }); this->itemsChangedTimer_.setInterval(100); this->itemsChangedTimer_.setSingleShot(true); } - virtual ~ReadOnlySignalVector() = default; - pajlada::Signals::Signal> itemInserted; - pajlada::Signals::Signal> itemRemoved; - pajlada::Signals::NoArgSignal delayedItemsChanged; - - Iterator begin() const + SignalVector(std::function &&compare) + : SignalVector() { - return Iterator( - const_cast &>(this->vector_).begin(), - this->mutex_); + itemCompare_ = std::move(compare); } - Iterator end() const + virtual bool isSorted() const { - return Iterator( - const_cast &>(this->vector_).end(), - this->mutex_); + return bool(this->itemCompare_); } - bool empty() const + /// A read-only version of the vector which can be used concurrently. + std::shared_ptr> readOnly() { - std::shared_lock lock(this->mutex_); - - return this->vector_.empty(); + return this->readOnly_; } - const std::vector &getVector() const + /// This may only be called from the GUI thread. + /// + /// @param item + /// Item to be inserted. + /// @param proposedIndex + /// Index to insert at. `-1` will append at the end. + /// Will be ignored if the vector is sorted. + /// @param caller + /// Caller id which will be passed in the itemInserted and itemRemoved + /// signals. + int insert(const T &item, int index = -1, void *caller = nullptr) { assertInGuiThread(); - return this->vector_; + if (this->isSorted()) + { + auto it = std::lower_bound(this->items_.begin(), this->items_.end(), + item, this->itemCompare_); + index = it - this->items_.begin(); + this->items_.insert(it, item); + } + else + { + if (index == -1) + index = this->items_.size(); + else + assert(index >= 0 && index <= this->items_.size()); + + this->items_.insert(this->items_.begin() + index, item); + } + + SignalVectorItemEvent args{item, index, caller}; + this->itemInserted.invoke(args); + this->itemsChanged_(); + + return index; } - std::vector cloneVector() const + /// This may only be called from the GUI thread. + /// + /// @param item + /// Item to be appended. + /// @param caller + /// Caller id which will be passed in the itemInserted and itemRemoved + /// signals. + int append(const T &item, void *caller = nullptr) { - std::shared_lock lock(this->mutex_); - - return this->vector_; + return this->insert(item, -1, caller); } - void invokeDelayedItemsChanged() + void removeAt(int index, void *caller = nullptr) + { + assertInGuiThread(); + assert(index >= 0 && index < int(this->items_.size())); + + T item = this->items_[index]; + this->items_.erase(this->items_.begin() + index); + + SignalVectorItemEvent args{item, index, caller}; + this->itemRemoved.invoke(args); + + this->itemsChanged_(); + } + + const std::vector &raw() const { assertInGuiThread(); + return this->items_; + } + + [[deprecated]] std::vector cloneVector() + { + return *this->readOnly(); + } + + // mirror vector functions + auto begin() const + { + assertInGuiThread(); + return this->items_.begin(); + } + + auto end() const + { + assertInGuiThread(); + return this->items_.end(); + } + + decltype(auto) operator[](size_t index) + { + assertInGuiThread(); + return this->items[index]; + } + + auto empty() + { + assertInGuiThread(); + return this->items_.empty(); + } + +private: + void itemsChanged_() + { + // emit delayed event if (!this->itemsChangedTimer_.isActive()) { this->itemsChangedTimer_.start(); } + + // update concurrent version + this->readOnly_ = std::make_shared>(this->items_); } - virtual bool isSorted() const = 0; - -protected: - std::vector vector_; + std::vector items_; + std::shared_ptr> readOnly_; QTimer itemsChangedTimer_; - mutable std::shared_mutex mutex_; -}; - -template -class BaseSignalVector : public ReadOnlySignalVector -{ -public: - // returns the actual index of the inserted item - virtual int insertItem(const TVectorItem &item, int proposedIndex = -1, - void *caller = nullptr) = 0; - - void removeItem(int index, void *caller = nullptr) - { - assertInGuiThread(); - std::unique_lock lock(this->mutex_); - - assert(index >= 0 && index < int(this->vector_.size())); - - TVectorItem item = this->vector_[index]; - - this->vector_.erase(this->vector_.begin() + index); - lock.unlock(); // manual unlock - - SignalVectorItemArgs args{item, index, caller}; - this->itemRemoved.invoke(args); - - this->invokeDelayedItemsChanged(); - } - - int appendItem(const TVectorItem &item, void *caller = nullptr) - { - return this->insertItem(item, -1, caller); - } -}; - -template -class UnsortedSignalVector : public BaseSignalVector -{ -public: - virtual int insertItem(const TVectorItem &item, int index = -1, - void *caller = nullptr) override - { - assertInGuiThread(); - - { - std::unique_lock lock(this->mutex_); - if (index == -1) - { - index = this->vector_.size(); - } - else - { - assert(index >= 0 && index <= this->vector_.size()); - } - - this->vector_.insert(this->vector_.begin() + index, item); - } - - SignalVectorItemArgs args{item, index, caller}; - this->itemInserted.invoke(args); - this->invokeDelayedItemsChanged(); - return index; - } - - virtual bool isSorted() const override - { - return false; - } -}; - -template -class SortedSignalVector : public BaseSignalVector -{ -public: - virtual int insertItem(const TVectorItem &item, int = -1, - void *caller = nullptr) override - { - assertInGuiThread(); - int index = -1; - - { - std::unique_lock lock(this->mutex_); - - auto it = std::lower_bound(this->vector_.begin(), - this->vector_.end(), item, Compare{}); - index = it - this->vector_.begin(); - this->vector_.insert(it, item); - } - - SignalVectorItemArgs args{item, index, caller}; - this->itemInserted.invoke(args); - this->invokeDelayedItemsChanged(); - return index; - } - - virtual bool isSorted() const override - { - return true; - } + std::function itemCompare_; }; } // namespace chatterino diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index adea7884..76bc14b2 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -25,11 +25,11 @@ public: } } - void init(BaseSignalVector *vec) + void initialize(SignalVector *vec) { this->vector_ = vec; - auto insert = [this](const SignalVectorItemArgs &args) { + auto insert = [this](const SignalVectorItemEvent &args) { if (args.caller == this) { return; @@ -52,9 +52,9 @@ public: }; int i = 0; - for (const TVectorItem &item : vec->getVector()) + for (const TVectorItem &item : vec->raw()) { - SignalVectorItemArgs args{item, i++, 0}; + SignalVectorItemEvent args{item, i++, 0}; insert(args); } @@ -89,6 +89,12 @@ public: this->afterInit(); } + SignalVectorModel *initialized(SignalVector *vec) + { + this->initialize(vec); + return this; + } + virtual ~SignalVectorModel() { for (Row &row : this->rows_) @@ -147,12 +153,12 @@ public: else { int vecRow = this->getVectorIndexFromModelIndex(row); - this->vector_->removeItem(vecRow, this); + this->vector_->removeAt(vecRow, this); assert(this->rows_[row].original); TVectorItem item = this->getItemFromRow( this->rows_[row].items, this->rows_[row].original.get()); - this->vector_->insertItem(item, vecRow, this); + this->vector_->insert(item, vecRow, this); } return true; @@ -219,7 +225,7 @@ public: void deleteRow(int row) { int signalVectorRow = this->getVectorIndexFromModelIndex(row); - this->vector_->removeItem(signalVectorRow); + this->vector_->removeAt(signalVectorRow); } bool removeRows(int row, int count, const QModelIndex &parent) override @@ -234,11 +240,71 @@ public: assert(row >= 0 && row < this->rows_.size()); int signalVectorRow = this->getVectorIndexFromModelIndex(row); - this->vector_->removeItem(signalVectorRow); + this->vector_->removeAt(signalVectorRow); return true; } + QStringList mimeTypes() const override + { + return {"chatterino_row_id"}; + } + + QMimeData *mimeData(const QModelIndexList &list) const + { + if (list.length() == 1) + { + return nullptr; + } + + // Check if all indices are in the same row -> single row selected + for (auto &&x : list) + { + if (x.row() != list.first().row()) + return nullptr; + } + + auto data = new QMimeData; + data->setData("chatterino_row_id", QByteArray::number(list[0].row())); + return data; + } + + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int /*row*/, + int /*column*/, const QModelIndex &parent) override + { + if (data->hasFormat("chatterino_row_id") && + action & (Qt::DropAction::MoveAction | Qt::DropAction::CopyAction)) + { + int from = data->data("chatterino_row_id").toInt(); + int to = parent.row(); + + if (from < 0 || from > this->vector_->raw().size() || to < 0 || + to > this->vector_->raw().size()) + { + return false; + } + + if (from != to) + { + auto item = this->vector_->raw()[from]; + this->vector_->removeAt(from); + this->vector_->insert(item, to); + } + + // We return false since we remove items ourselves. + return false; + } + + return false; + } + + Qt::DropActions supportedDropActions() const override + { + return this->vector_->isSorted() + ? Qt::DropActions() + : Qt::DropAction::CopyAction | Qt::DropAction::MoveAction; + } + protected: virtual void afterInit() { @@ -326,7 +392,7 @@ protected: private: std::vector> headerData_; - BaseSignalVector *vector_; + SignalVector *vector_; std::vector rows_; int columnCount_; diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index cdaa5328..2692b05a 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -7,20 +7,20 @@ namespace chatterino { AccountController::AccountController() + : accounts_(SharedPtrElementLess{}) { this->twitch.accounts.itemInserted.connect([this](const auto &args) { - this->accounts_.insertItem( - std::dynamic_pointer_cast(args.item)); + this->accounts_.insert(std::dynamic_pointer_cast(args.item)); }); this->twitch.accounts.itemRemoved.connect([this](const auto &args) { if (args.caller != this) { - auto &accs = this->twitch.accounts.getVector(); + auto &accs = this->twitch.accounts.raw(); auto it = std::find(accs.begin(), accs.end(), args.item); assert(it != accs.end()); - this->accounts_.removeItem(it - accs.begin(), this); + this->accounts_.removeAt(it - accs.begin(), this); } }); @@ -30,10 +30,10 @@ AccountController::AccountController() case ProviderId::Twitch: { if (args.caller != this) { - auto accs = this->twitch.accounts.cloneVector(); + auto &&accs = this->twitch.accounts; auto it = std::find(accs.begin(), accs.end(), args.item); assert(it != accs.end()); - this->twitch.accounts.removeItem(it - accs.begin(), this); + this->twitch.accounts.removeAt(it - accs.begin(), this); } } break; @@ -50,7 +50,7 @@ AccountModel *AccountController::createModel(QObject *parent) { AccountModel *model = new AccountModel(parent); - model->init(&this->accounts_); + model->initialize(&this->accounts_); return model; } diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp index e297b9e4..e1ef3c70 100644 --- a/src/controllers/accounts/AccountController.hpp +++ b/src/controllers/accounts/AccountController.hpp @@ -27,8 +27,7 @@ public: TwitchAccountManager twitch; private: - SortedSignalVector, SharedPtrElementLess> - accounts_; + SignalVector> accounts_; }; } // namespace chatterino diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index a6150cfa..fa8a08e9 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -8,9 +8,9 @@ #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "messages/MessageElement.hpp" -#include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" @@ -212,7 +212,7 @@ void CommandController::initialize(Settings &, Paths &paths) // Update the setting when the vector of commands has been updated (most // likely from the settings dialog) this->items_.delayedItemsChanged.connect([this] { // - this->commandsSetting_->setValue(this->items_.getVector()); + this->commandsSetting_->setValue(this->items_.raw()); }); // Load commands from commands.json @@ -222,7 +222,7 @@ void CommandController::initialize(Settings &, Paths &paths) // of commands) for (const auto &command : this->commandsSetting_->getValue()) { - this->items_.appendItem(command); + this->items_.append(command); } } @@ -234,7 +234,7 @@ void CommandController::save() CommandModel *CommandController::createModel(QObject *parent) { CommandModel *model = new CommandModel(parent); - model->init(&this->items_); + model->initialize(&this->items_); return model; } @@ -245,8 +245,6 @@ QString CommandController::execCommand(const QString &textNoEmoji, QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji); QStringList words = text.split(' ', QString::SkipEmptyParts); - std::lock_guard lock(this->mutex_); - if (words.length() == 0) { return text; @@ -366,18 +364,17 @@ QString CommandController::execCommand(const QString &textNoEmoji, return ""; } - TwitchApi::findUserId( - target, [user, channel, target](QString userId) { - if (userId.isEmpty()) - { - channel->addMessage(makeSystemMessage( - "User " + target + " could not be followed!")); - return; - } - user->followUser(userId, [channel, target]() { + getHelix()->getUserByName( + target, + [user, channel, target](const auto &targetUser) { + user->followUser(targetUser.id, [channel, target]() { channel->addMessage(makeSystemMessage( "You successfully followed " + target)); }); + }, + [channel, target] { + channel->addMessage(makeSystemMessage( + "User " + target + " could not be followed!")); }); return ""; @@ -402,18 +399,17 @@ QString CommandController::execCommand(const QString &textNoEmoji, return ""; } - TwitchApi::findUserId( - target, [user, channel, target](QString userId) { - if (userId.isEmpty()) - { - channel->addMessage(makeSystemMessage( - "User " + target + " could not be followed!")); - return; - } - user->unfollowUser(userId, [channel, target]() { + getHelix()->getUserByName( + target, + [user, channel, target](const auto &targetUser) { + user->unfollowUser(targetUser.id, [channel, target]() { channel->addMessage(makeSystemMessage( "You successfully unfollowed " + target)); }); + }, + [channel, target] { + channel->addMessage(makeSystemMessage( + "User " + target + " could not be followed!")); }); return ""; diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index 938fe0f3..ebc57d8c 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -22,7 +22,7 @@ class CommandModel; class CommandController final : public Singleton { public: - UnsortedSignalVector items_; + SignalVector items_; QString execCommand(const QString &text, std::shared_ptr channel, bool dryRun); @@ -39,8 +39,6 @@ private: QMap commandsMap_; int maxSpaces_ = 0; - std::mutex mutex_; - std::shared_ptr sm_; // Because the setting manager is not initialized until the initialize // function is called (and not in the constructor), we have to diff --git a/src/controllers/commands/CommandModel.cpp b/src/controllers/commands/CommandModel.cpp index 4a55907d..d12a81c6 100644 --- a/src/controllers/commands/CommandModel.cpp +++ b/src/controllers/commands/CommandModel.cpp @@ -1,5 +1,7 @@ #include "CommandModel.hpp" +#include "util/StandardItemHelper.hpp" + namespace chatterino { // commandmodel @@ -20,12 +22,8 @@ Command CommandModel::getItemFromRow(std::vector &row, void CommandModel::getRowFromItem(const Command &item, std::vector &row) { - row[0]->setData(item.name, Qt::DisplayRole); - row[0]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | - Qt::ItemIsEditable); - row[1]->setData(item.func, Qt::DisplayRole); - row[1]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | - Qt::ItemIsEditable); + setStringItem(row[0], item.name); + setStringItem(row[1], item.func); } } // namespace chatterino diff --git a/src/controllers/highlights/HighlightBlacklistModel.hpp b/src/controllers/highlights/HighlightBlacklistModel.hpp index c3420b06..d4acc474 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.hpp +++ b/src/controllers/highlights/HighlightBlacklistModel.hpp @@ -11,9 +11,9 @@ class HighlightController; class HighlightBlacklistModel : public SignalVectorModel { +public: explicit HighlightBlacklistModel(QObject *parent); -public: enum Column { Pattern = 0, UseRegex = 1, @@ -28,8 +28,6 @@ protected: // turns a row in the model into a vector item virtual void getRowFromItem(const HighlightBlacklistUser &item, std::vector &row) override; - - friend class HighlightController; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp deleted file mode 100644 index d030f77b..00000000 --- a/src/controllers/highlights/HighlightController.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "HighlightController.hpp" - -#include "Application.hpp" -#include "controllers/highlights/HighlightBlacklistModel.hpp" -#include "controllers/highlights/HighlightModel.hpp" -#include "controllers/highlights/UserHighlightModel.hpp" -#include "widgets/dialogs/NotificationPopup.hpp" - -namespace chatterino { - -HighlightController::HighlightController() -{ -} - -void HighlightController::initialize(Settings &settings, Paths &paths) -{ - assert(!this->initialized_); - this->initialized_ = true; - - for (const HighlightPhrase &phrase : this->highlightsSetting_.getValue()) - { - this->phrases.appendItem(phrase); - } - - this->phrases.delayedItemsChanged.connect([this] { // - this->highlightsSetting_.setValue(this->phrases.getVector()); - }); - - for (const HighlightBlacklistUser &blacklistedUser : - this->blacklistSetting_.getValue()) - { - this->blacklistedUsers.appendItem(blacklistedUser); - } - - this->blacklistedUsers.delayedItemsChanged.connect([this] { - this->blacklistSetting_.setValue(this->blacklistedUsers.getVector()); - }); - - for (const HighlightPhrase &user : this->userSetting_.getValue()) - { - this->highlightedUsers.appendItem(user); - } - - this->highlightedUsers.delayedItemsChanged.connect([this] { // - this->userSetting_.setValue(this->highlightedUsers.getVector()); - }); -} - -HighlightModel *HighlightController::createModel(QObject *parent) -{ - HighlightModel *model = new HighlightModel(parent); - model->init(&this->phrases); - - return model; -} - -UserHighlightModel *HighlightController::createUserModel(QObject *parent) -{ - auto *model = new UserHighlightModel(parent); - model->init(&this->highlightedUsers); - - return model; -} - -bool HighlightController::isHighlightedUser(const QString &username) -{ - const auto &userItems = this->highlightedUsers; - for (const auto &highlightedUser : userItems) - { - if (highlightedUser.isMatch(username)) - { - return true; - } - } - - return false; -} - -HighlightBlacklistModel *HighlightController::createBlacklistModel( - QObject *parent) -{ - auto *model = new HighlightBlacklistModel(parent); - model->init(&this->blacklistedUsers); - - return model; -} - -bool HighlightController::blacklistContains(const QString &username) -{ - for (const auto &blacklistedUser : this->blacklistedUsers) - { - if (blacklistedUser.isMatch(username)) - { - return true; - } - } - - return false; -} - -void HighlightController::addHighlight(const MessagePtr &msg) -{ - // static NotificationPopup popup; - - // popup.updatePosition(); - // popup.addMessage(msg); - // popup.show(); -} - -} // namespace chatterino diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp deleted file mode 100644 index faa4683c..00000000 --- a/src/controllers/highlights/HighlightController.hpp +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -#include "common/ChatterinoSetting.hpp" -#include "common/SignalVector.hpp" -#include "common/Singleton.hpp" -#include "controllers/highlights/HighlightBlacklistUser.hpp" -#include "controllers/highlights/HighlightPhrase.hpp" - -namespace chatterino { - -struct Message; -using MessagePtr = std::shared_ptr; - -class Settings; -class Paths; - -class UserHighlightModel; -class HighlightModel; -class HighlightBlacklistModel; - -class HighlightController final : public Singleton -{ -public: - HighlightController(); - - virtual void initialize(Settings &settings, Paths &paths) override; - - UnsortedSignalVector phrases; - UnsortedSignalVector blacklistedUsers; - UnsortedSignalVector highlightedUsers; - - HighlightModel *createModel(QObject *parent); - HighlightBlacklistModel *createBlacklistModel(QObject *parent); - UserHighlightModel *createUserModel(QObject *parent); - - bool isHighlightedUser(const QString &username); - bool blacklistContains(const QString &username); - - void addHighlight(const MessagePtr &msg); - -private: - bool initialized_ = false; - - ChatterinoSetting> highlightsSetting_ = { - "/highlighting/highlights"}; - ChatterinoSetting> blacklistSetting_ = { - "/highlighting/blacklist"}; - ChatterinoSetting> userSetting_ = { - "/highlighting/users"}; -}; - -} // namespace chatterino diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index 05f37bf7..e1b6e410 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "singletons/Settings.hpp" +#include "singletons/WindowManager.hpp" #include "util/StandardItemHelper.hpp" namespace chatterino { @@ -63,10 +64,10 @@ void HighlightModel::afterInit() usernameRow[Column::CaseSensitive]->setFlags(0); QUrl selfSound = QUrl(getSettings()->selfHighlightSoundUrl.getValue()); - setFilePathItem(usernameRow[Column::SoundPath], selfSound); + setFilePathItem(usernameRow[Column::SoundPath], selfSound, false); auto selfColor = ColorProvider::instance().color(ColorType::SelfHighlight); - setColorItem(usernameRow[Column::Color], *selfColor); + setColorItem(usernameRow[Column::Color], *selfColor, false); this->insertCustomRow(usernameRow, 0); @@ -86,12 +87,13 @@ void HighlightModel::afterInit() QUrl whisperSound = QUrl(getSettings()->whisperHighlightSoundUrl.getValue()); - setFilePathItem(whisperRow[Column::SoundPath], whisperSound); + setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false); - auto whisperColor = ColorProvider::instance().color(ColorType::Whisper); - setColorItem(whisperRow[Column::Color], *whisperColor); + // auto whisperColor = ColorProvider::instance().color(ColorType::Whisper); + // setColorItem(whisperRow[Column::Color], *whisperColor, false); + whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags); - this->insertCustomRow(whisperRow, 1); + this->insertCustomRow(whisperRow, WHISPER_ROW); // Highlight settings for subscription messages std::vector subRow = this->createRow(); @@ -107,12 +109,37 @@ void HighlightModel::afterInit() subRow[Column::CaseSensitive]->setFlags(0); QUrl subSound = QUrl(getSettings()->subHighlightSoundUrl.getValue()); - setFilePathItem(subRow[Column::SoundPath], subSound); + setFilePathItem(subRow[Column::SoundPath], subSound, false); auto subColor = ColorProvider::instance().color(ColorType::Subscription); - setColorItem(subRow[Column::Color], *subColor); + setColorItem(subRow[Column::Color], *subColor, false); this->insertCustomRow(subRow, 2); + + // Highlight settings for redeemed highlight messages + std::vector redeemedRow = this->createRow(); + setBoolItem(redeemedRow[Column::Pattern], + getSettings()->enableRedeemedHighlight.getValue(), true, false); + redeemedRow[Column::Pattern]->setData("Highlights redeemed with Bits", + Qt::DisplayRole); + setBoolItem(redeemedRow[Column::FlashTaskbar], + getSettings()->enableRedeemedHighlightTaskbar.getValue(), true, + false); + setBoolItem(redeemedRow[Column::PlaySound], + getSettings()->enableRedeemedHighlightSound.getValue(), true, + false); + redeemedRow[Column::UseRegex]->setFlags(0); + redeemedRow[Column::CaseSensitive]->setFlags(0); + + QUrl RedeemedSound = + QUrl(getSettings()->redeemedHighlightSoundUrl.getValue()); + setFilePathItem(redeemedRow[Column::SoundPath], RedeemedSound, false); + + auto RedeemedColor = + ColorProvider::instance().color(ColorType::RedeemedHighlight); + setColorItem(redeemedRow[Column::Color], *RedeemedColor, false); + + this->insertCustomRow(redeemedRow, 3); } void HighlightModel::customRowSetData(const std::vector &row, @@ -128,7 +155,7 @@ void HighlightModel::customRowSetData(const std::vector &row, { getSettings()->enableSelfHighlight.setValue(value.toBool()); } - else if (rowIndex == 1) + else if (rowIndex == WHISPER_ROW) { getSettings()->enableWhisperHighlight.setValue( value.toBool()); @@ -137,6 +164,11 @@ void HighlightModel::customRowSetData(const std::vector &row, { getSettings()->enableSubHighlight.setValue(value.toBool()); } + else if (rowIndex == 3) + { + getSettings()->enableRedeemedHighlight.setValue( + value.toBool()); + } } } break; @@ -148,7 +180,7 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->enableSelfHighlightTaskbar.setValue( value.toBool()); } - else if (rowIndex == 1) + else if (rowIndex == WHISPER_ROW) { getSettings()->enableWhisperHighlightTaskbar.setValue( value.toBool()); @@ -158,6 +190,11 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->enableSubHighlightTaskbar.setValue( value.toBool()); } + else if (rowIndex == 3) + { + getSettings()->enableRedeemedHighlightTaskbar.setValue( + value.toBool()); + } } } break; @@ -169,7 +206,7 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->enableSelfHighlightSound.setValue( value.toBool()); } - else if (rowIndex == 1) + else if (rowIndex == WHISPER_ROW) { getSettings()->enableWhisperHighlightSound.setValue( value.toBool()); @@ -179,6 +216,11 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->enableSubHighlightSound.setValue( value.toBool()); } + else if (rowIndex == 3) + { + getSettings()->enableRedeemedHighlightSound.setValue( + value.toBool()); + } } } break; @@ -199,7 +241,7 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->selfHighlightSoundUrl.setValue( value.toString()); } - else if (rowIndex == 1) + else if (rowIndex == WHISPER_ROW) { getSettings()->whisperHighlightSoundUrl.setValue( value.toString()); @@ -209,6 +251,11 @@ void HighlightModel::customRowSetData(const std::vector &row, getSettings()->subHighlightSoundUrl.setValue( value.toString()); } + else if (rowIndex == 3) + { + getSettings()->redeemedHighlightSoundUrl.setValue( + value.toString()); + } } } break; @@ -221,18 +268,27 @@ void HighlightModel::customRowSetData(const std::vector &row, { getSettings()->selfHighlightColor.setValue(colorName); } - else if (rowIndex == 1) - { - getSettings()->whisperHighlightColor.setValue(colorName); - } + // else if (rowIndex == WHISPER_ROW) + // { + // getSettings()->whisperHighlightColor.setValue(colorName); + // } else if (rowIndex == 2) { getSettings()->subHighlightColor.setValue(colorName); } + else if (rowIndex == 3) + { + getSettings()->redeemedHighlightColor.setValue(colorName); + const_cast(ColorProvider::instance()) + .updateColor(ColorType::RedeemedHighlight, + QColor(colorName)); + } } } break; } + + getApp()->windows->forceLayoutChannelViews(); } } // namespace chatterino diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp index 13b85bca..92acd2e0 100644 --- a/src/controllers/highlights/HighlightModel.hpp +++ b/src/controllers/highlights/HighlightModel.hpp @@ -7,13 +7,11 @@ namespace chatterino { -class HighlightController; - class HighlightModel : public SignalVectorModel { +public: explicit HighlightModel(QObject *parent); -public: // Used here, in HighlightingPage and in UserHighlightModel enum Column { Pattern = 0, @@ -25,6 +23,8 @@ public: Color = 6 }; + constexpr static int WHISPER_ROW = 1; + protected: // turn a vector item into a model row virtual HighlightPhrase getItemFromRow( @@ -40,8 +40,6 @@ protected: virtual void customRowSetData(const std::vector &row, int column, const QVariant &value, int role, int rowIndex) override; - - friend class HighlightController; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index 40942df3..5ffd5b36 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -2,6 +2,11 @@ namespace chatterino { +QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127); +QColor HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR = + QColor(28, 126, 141, 90); +QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100); + bool HighlightPhrase::operator==(const HighlightPhrase &other) const { return std::tie(this->pattern_, this->hasSound_, this->hasAlert_, diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index 72849bb6..268ae354 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -70,6 +70,14 @@ public: const QUrl &getSoundUrl() const; const std::shared_ptr getColor() const; + /* + * XXX: Use the constexpr constructor here once we are building with + * Qt>=5.13. + */ + static QColor FALLBACK_HIGHLIGHT_COLOR; + static QColor FALLBACK_REDEEMED_HIGHLIGHT_COLOR; + static QColor FALLBACK_SUB_COLOR; + private: QString pattern_; bool hasAlert_; @@ -132,6 +140,8 @@ struct Deserialize { chatterino::rj::getSafe(value, "color", encodedColor); auto _color = QColor(encodedColor); + if (!_color.isValid()) + _color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR; return chatterino::HighlightPhrase(_pattern, _hasAlert, _hasSound, _isRegex, _isCaseSensitive, diff --git a/src/controllers/highlights/UserHighlightModel.hpp b/src/controllers/highlights/UserHighlightModel.hpp index dcc42a95..de433243 100644 --- a/src/controllers/highlights/UserHighlightModel.hpp +++ b/src/controllers/highlights/UserHighlightModel.hpp @@ -11,6 +11,7 @@ class HighlightController; class UserHighlightModel : public SignalVectorModel { +public: explicit UserHighlightModel(QObject *parent); protected: @@ -21,8 +22,6 @@ protected: virtual void getRowFromItem(const HighlightPhrase &item, std::vector &row) override; - - friend class HighlightController; }; } // namespace chatterino diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp deleted file mode 100644 index 5548ee5d..00000000 --- a/src/controllers/ignores/IgnoreController.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "controllers/ignores/IgnoreController.hpp" - -#include "Application.hpp" -#include "controllers/ignores/IgnoreModel.hpp" - -#include - -namespace chatterino { - -void IgnoreController::initialize(Settings &, Paths &) -{ - assert(!this->initialized_); - this->initialized_ = true; - - for (const IgnorePhrase &phrase : this->ignoresSetting_.getValue()) - { - this->phrases.appendItem(phrase); - } - - this->phrases.delayedItemsChanged.connect([this] { // - this->ignoresSetting_.setValue(this->phrases.getVector()); - }); -} - -IgnoreModel *IgnoreController::createModel(QObject *parent) -{ - IgnoreModel *model = new IgnoreModel(parent); - model->init(&this->phrases); - - return model; -} - -} // namespace chatterino diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index 1279f4a8..fed12f12 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -1,33 +1,7 @@ #pragma once -#include "common/ChatterinoSetting.hpp" -#include "common/SignalVector.hpp" -#include "common/Singleton.hpp" -#include "controllers/ignores/IgnorePhrase.hpp" - namespace chatterino { -class Settings; -class Paths; - -class IgnoreModel; - enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster }; -class IgnoreController final : public Singleton -{ -public: - virtual void initialize(Settings &settings, Paths &paths) override; - - UnsortedSignalVector phrases; - - IgnoreModel *createModel(QObject *parent); - -private: - bool initialized_ = false; - - ChatterinoSetting> ignoresSetting_ = { - "/ignore/phrases"}; -}; - } // namespace chatterino diff --git a/src/controllers/ignores/IgnoreModel.hpp b/src/controllers/ignores/IgnoreModel.hpp index 1b9a5099..473b61b9 100644 --- a/src/controllers/ignores/IgnoreModel.hpp +++ b/src/controllers/ignores/IgnoreModel.hpp @@ -7,10 +7,9 @@ namespace chatterino { -class IgnoreController; - class IgnoreModel : public SignalVectorModel { +public: explicit IgnoreModel(QObject *parent); protected: @@ -21,8 +20,6 @@ protected: // turns a row in the model into a vector item virtual void getRowFromItem(const IgnorePhrase &item, std::vector &row) override; - - friend class IgnoreController; }; } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index dce01904..d94cb234 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -71,11 +71,11 @@ ModerationAction::ModerationAction(const QString &action) } else if (action.startsWith("/ban ")) { - this->image_ = Image::fromPixmap(getResources().buttons.ban); + this->imageToLoad_ = 1; } else if (action.startsWith("/delete ")) { - this->image_ = Image::fromPixmap(getResources().buttons.trashCan); + this->imageToLoad_ = 2; } else { @@ -100,6 +100,16 @@ bool ModerationAction::isImage() const const boost::optional &ModerationAction::getImage() const { + assertInGuiThread(); + + if (this->imageToLoad_ != 0) + { + if (this->imageToLoad_ == 1) + this->image_ = Image::fromPixmap(getResources().buttons.ban); + else if (this->imageToLoad_ == 2) + this->image_ = Image::fromPixmap(getResources().buttons.trashCan); + } + return this->image_; } diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index eed7579a..3b4532d9 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -25,10 +25,11 @@ public: const QString &getAction() const; private: - boost::optional image_; + mutable boost::optional image_; QString line1_; QString line2_; QString action_; + int imageToLoad_{}; }; } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp index e9f37236..e13b3b27 100644 --- a/src/controllers/moderationactions/ModerationActionModel.hpp +++ b/src/controllers/moderationactions/ModerationActionModel.hpp @@ -7,8 +7,6 @@ namespace chatterino { -class ModerationActions; - class ModerationActionModel : public SignalVectorModel { public: @@ -25,8 +23,6 @@ protected: std::vector &row) override; friend class HighlightController; - - friend class ModerationActions; }; } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActions.cpp b/src/controllers/moderationactions/ModerationActions.cpp deleted file mode 100644 index 0104c7de..00000000 --- a/src/controllers/moderationactions/ModerationActions.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "ModerationActions.hpp" - -#include "Application.hpp" -#include "controllers/moderationactions/ModerationActionModel.hpp" -#include "singletons/Settings.hpp" - -#include - -namespace chatterino { - -ModerationActions::ModerationActions() -{ -} - -void ModerationActions::initialize(Settings &settings, Paths &paths) -{ - assert(!this->initialized_); - this->initialized_ = true; - - this->setting_ = - std::make_unique>>( - "/moderation/actions"); - - for (auto &val : this->setting_->getValue()) - { - this->items.insertItem(val); - } - - this->items.delayedItemsChanged.connect([this] { // - this->setting_->setValue(this->items.getVector()); - }); -} - -ModerationActionModel *ModerationActions::createModel(QObject *parent) -{ - ModerationActionModel *model = new ModerationActionModel(parent); - model->init(&this->items); - - return model; -} - -} // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationActions.hpp b/src/controllers/moderationactions/ModerationActions.hpp deleted file mode 100644 index e1bf1b80..00000000 --- a/src/controllers/moderationactions/ModerationActions.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "common/Singleton.hpp" - -#include "common/ChatterinoSetting.hpp" -#include "common/SignalVector.hpp" -#include "controllers/moderationactions/ModerationAction.hpp" - -namespace chatterino { - -class Settings; -class Paths; - -class ModerationActionModel; - -class ModerationActions final : public Singleton -{ -public: - ModerationActions(); - - virtual void initialize(Settings &settings, Paths &paths) override; - - UnsortedSignalVector items; - - ModerationActionModel *createModel(QObject *parent); - -private: - std::unique_ptr>> setting_; - bool initialized_ = false; -}; - -} // namespace chatterino diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index b9777c80..2578558d 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -4,8 +4,8 @@ #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" #include "controllers/notifications/NotificationModel.hpp" -#include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "providers/twitch/api/Helix.hpp" #include "singletons/Toasts.hpp" #include "singletons/WindowManager.hpp" #include "widgets/Window.hpp" @@ -26,12 +26,11 @@ void NotificationController::initialize(Settings &settings, Paths &paths) this->initialized_ = true; for (const QString &channelName : this->twitchSetting_.getValue()) { - this->channelMap[Platform::Twitch].appendItem(channelName); + this->channelMap[Platform::Twitch].append(channelName); } this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { // - this->twitchSetting_.setValue( - this->channelMap[Platform::Twitch].getVector()); + this->twitchSetting_.setValue(this->channelMap[Platform::Twitch].raw()); }); /* for (const QString &channelName : this->mixerSetting_.getValue()) { @@ -81,18 +80,18 @@ bool NotificationController::isChannelNotified(const QString &channelName, void NotificationController::addChannelNotification(const QString &channelName, Platform p) { - channelMap[p].appendItem(channelName); + channelMap[p].append(channelName); } void NotificationController::removeChannelNotification( const QString &channelName, Platform p) { - for (std::vector::size_type i = 0; - i != channelMap[p].getVector().size(); i++) + for (std::vector::size_type i = 0; i != channelMap[p].raw().size(); + i++) { - if (channelMap[p].getVector()[i].toLower() == channelName.toLower()) + if (channelMap[p].raw()[i].toLower() == channelName.toLower()) { - channelMap[p].removeItem(i); + channelMap[p].removeAt(i); i--; } } @@ -121,21 +120,21 @@ NotificationModel *NotificationController::createModel(QObject *parent, Platform p) { NotificationModel *model = new NotificationModel(parent); - model->init(&this->channelMap[p]); + model->initialize(&this->channelMap[p]); return model; } void NotificationController::fetchFakeChannels() { for (std::vector::size_type i = 0; - i != channelMap[Platform::Twitch].getVector().size(); i++) + i != channelMap[Platform::Twitch].raw().size(); i++) { auto chan = getApp()->twitch.server->getChannelOrEmpty( - channelMap[Platform::Twitch].getVector()[i]); + channelMap[Platform::Twitch].raw()[i]); if (chan->isEmpty()) { getFakeTwitchChannelLiveStatus( - channelMap[Platform::Twitch].getVector()[i]); + channelMap[Platform::Twitch].raw()[i]); } } } @@ -143,68 +142,52 @@ void NotificationController::fetchFakeChannels() void NotificationController::getFakeTwitchChannelLiveStatus( const QString &channelName) { - TwitchApi::findUserId(channelName, [channelName, this](QString roomID) { - if (roomID.isEmpty()) - { + getHelix()->getStreamByName( + channelName, + [channelName, this](bool live, const auto &stream) { + qDebug() << "[TwitchChannel" << channelName + << "] Refreshing live status"; + + if (!live) + { + // Stream is offline + this->removeFakeChannel(channelName); + return; + } + + // Stream is online + auto i = std::find(fakeTwitchChannels.begin(), + fakeTwitchChannels.end(), channelName); + + if (i != fakeTwitchChannels.end()) + { + // We have already pushed the live state of this stream + // Could not find stream in fake twitch channels! + return; + } + + if (Toasts::isEnabled()) + { + getApp()->toasts->sendChannelNotification(channelName, + Platform::Twitch); + } + if (getSettings()->notificationPlaySound) + { + getApp()->notifications->playSound(); + } + if (getSettings()->notificationFlashTaskbar) + { + getApp()->windows->sendAlert(); + } + + // Indicate that we have pushed notifications for this stream + fakeTwitchChannels.push_back(channelName); + }, + [channelName, this] { qDebug() << "[TwitchChannel" << channelName << "] Refreshing live status (Missing ID)"; - removeFakeChannel(channelName); - return; - } - qDebug() << "[TwitchChannel" << channelName - << "] Refreshing live status"; - - QString url("https://api.twitch.tv/kraken/streams/" + roomID); - NetworkRequest::twitchRequest(url) - .onSuccess([this, channelName](auto result) -> Outcome { - rapidjson::Document document = result.parseRapidJson(); - if (!document.IsObject()) - { - qDebug() << "[TwitchChannel:refreshLiveStatus] root is not " - "an object"; - return Failure; - } - - if (!document.HasMember("stream")) - { - qDebug() << "[TwitchChannel:refreshLiveStatus] Missing " - "stream in root"; - return Failure; - } - - const auto &stream = document["stream"]; - - if (!stream.IsObject()) - { - // Stream is offline (stream is most likely null) - // removeFakeChannel(channelName); - return Failure; - } - // Stream is live - auto i = std::find(fakeTwitchChannels.begin(), - fakeTwitchChannels.end(), channelName); - - if (!(i != fakeTwitchChannels.end())) - { - fakeTwitchChannels.push_back(channelName); - if (Toasts::isEnabled()) - { - getApp()->toasts->sendChannelNotification( - channelName, Platform::Twitch); - } - if (getSettings()->notificationPlaySound) - { - getApp()->notifications->playSound(); - } - if (getSettings()->notificationFlashTaskbar) - { - getApp()->windows->sendAlert(); - } - } - return Success; - }) - .execute(); - }); + this->removeFakeChannel(channelName); + }); } void NotificationController::removeFakeChannel(const QString channelName) diff --git a/src/controllers/notifications/NotificationController.hpp b/src/controllers/notifications/NotificationController.hpp index 228348ad..e8d0c4ef 100644 --- a/src/controllers/notifications/NotificationController.hpp +++ b/src/controllers/notifications/NotificationController.hpp @@ -30,9 +30,9 @@ public: void playSound(); - UnsortedSignalVector getVector(Platform p); + SignalVector getVector(Platform p); - std::map> channelMap; + std::map> channelMap; NotificationModel *createModel(QObject *parent, Platform p); @@ -43,6 +43,7 @@ private: void removeFakeChannel(const QString channelName); void getFakeTwitchChannelLiveStatus(const QString &channelName); + // fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for std::vector fakeTwitchChannels; QTimer *liveStatusTimer_; diff --git a/src/controllers/pings/MutedChannelModel.cpp b/src/controllers/pings/MutedChannelModel.cpp new file mode 100644 index 00000000..fc147350 --- /dev/null +++ b/src/controllers/pings/MutedChannelModel.cpp @@ -0,0 +1,28 @@ +#include "MutedChannelModel.hpp" + +#include "Application.hpp" +#include "singletons/Settings.hpp" +#include "util/StandardItemHelper.hpp" + +namespace chatterino { + +MutedChannelModel::MutedChannelModel(QObject *parent) + : SignalVectorModel(1, parent) +{ +} + +// turn a vector item into a model row +QString MutedChannelModel::getItemFromRow(std::vector &row, + const QString &original) +{ + return QString(row[0]->data(Qt::DisplayRole).toString()); +} + +// turn a model +void MutedChannelModel::getRowFromItem(const QString &item, + std::vector &row) +{ + setStringItem(row[0], item); +} + +} // namespace chatterino diff --git a/src/controllers/pings/PingModel.hpp b/src/controllers/pings/MutedChannelModel.hpp similarity index 79% rename from src/controllers/pings/PingModel.hpp rename to src/controllers/pings/MutedChannelModel.hpp index 137be5e0..c53b3177 100644 --- a/src/controllers/pings/PingModel.hpp +++ b/src/controllers/pings/MutedChannelModel.hpp @@ -7,11 +7,11 @@ namespace chatterino { -class PingController; +class MutedChannelController; -class PingModel : public SignalVectorModel +class MutedChannelModel : public SignalVectorModel { - explicit PingModel(QObject *parent); + explicit MutedChannelModel(QObject *parent); protected: // turn a vector item into a model row @@ -21,8 +21,6 @@ protected: // turns a row in the model into a vector item virtual void getRowFromItem(const QString &item, std::vector &row) override; - - friend class PingController; }; } // namespace chatterino diff --git a/src/controllers/pings/PingController.cpp b/src/controllers/pings/PingController.cpp deleted file mode 100644 index b61d6e79..00000000 --- a/src/controllers/pings/PingController.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "controllers/pings/PingController.hpp" -#include "controllers/pings/PingModel.hpp" - -namespace chatterino { - -void PingController::initialize(Settings &settings, Paths &paths) -{ - this->initialized_ = true; - for (const QString &channelName : this->pingSetting_.getValue()) - { - this->channelVector.appendItem(channelName); - } - - this->channelVector.delayedItemsChanged.connect([this] { // - this->pingSetting_.setValue(this->channelVector.getVector()); - }); -} - -PingModel *PingController::createModel(QObject *parent) -{ - PingModel *model = new PingModel(parent); - model->init(&this->channelVector); - return model; -} - -bool PingController::isMuted(const QString &channelName) -{ - for (const auto &channel : this->channelVector) - { - if (channelName.toLower() == channel.toLower()) - { - return true; - } - } - return false; -} - -void PingController::muteChannel(const QString &channelName) -{ - channelVector.appendItem(channelName); -} - -void PingController::unmuteChannel(const QString &channelName) -{ - for (std::vector::size_type i = 0; - i != channelVector.getVector().size(); i++) - { - if (channelVector.getVector()[i].toLower() == channelName.toLower()) - { - channelVector.removeItem(i); - i--; - } - } -} - -bool PingController::toggleMuteChannel(const QString &channelName) -{ - if (this->isMuted(channelName)) - { - unmuteChannel(channelName); - return false; - } - else - { - muteChannel(channelName); - return true; - } -} - -} // namespace chatterino diff --git a/src/controllers/pings/PingController.hpp b/src/controllers/pings/PingController.hpp deleted file mode 100644 index 80805e85..00000000 --- a/src/controllers/pings/PingController.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include - -#include "common/SignalVector.hpp" -#include "common/Singleton.hpp" -#include "singletons/Settings.hpp" - -namespace chatterino { - -class Settings; -class Paths; - -class PingModel; - -class PingController final : public Singleton, private QObject -{ -public: - virtual void initialize(Settings &settings, Paths &paths) override; - - bool isMuted(const QString &channelName); - void muteChannel(const QString &channelName); - void unmuteChannel(const QString &channelName); - bool toggleMuteChannel(const QString &channelName); - - PingModel *createModel(QObject *parent); - -private: - bool initialized_ = false; - - UnsortedSignalVector channelVector; - - ChatterinoSetting> pingSetting_ = {"/pings/muted"}; -}; - -} // namespace chatterino diff --git a/src/controllers/pings/PingModel.cpp b/src/controllers/pings/PingModel.cpp deleted file mode 100644 index 28098a20..00000000 --- a/src/controllers/pings/PingModel.cpp +++ /dev/null @@ -1,28 +0,0 @@ -#include "PingModel.hpp" - -#include "Application.hpp" -#include "singletons/Settings.hpp" -#include "util/StandardItemHelper.hpp" - -namespace chatterino { - -PingModel::PingModel(QObject *parent) - : SignalVectorModel(1, parent) -{ -} - -// turn a vector item into a model row -QString PingModel::getItemFromRow(std::vector &row, - const QString &original) -{ - return QString(row[0]->data(Qt::DisplayRole).toString()); -} - -// turn a model -void PingModel::getRowFromItem(const QString &item, - std::vector &row) -{ - setStringItem(row[0], item); -} - -} // namespace chatterino diff --git a/src/controllers/taggedusers/TaggedUsersController.cpp b/src/controllers/taggedusers/TaggedUsersController.cpp deleted file mode 100644 index 2aa82a6c..00000000 --- a/src/controllers/taggedusers/TaggedUsersController.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "TaggedUsersController.hpp" - -#include "controllers/taggedusers/TaggedUsersModel.hpp" - -namespace chatterino { - -TaggedUsersController::TaggedUsersController() -{ -} - -TaggedUsersModel *TaggedUsersController::createModel(QObject *parent) -{ - TaggedUsersModel *model = new TaggedUsersModel(parent); - model->init(&this->users); - - return model; -} - -} // namespace chatterino diff --git a/src/controllers/taggedusers/TaggedUsersController.hpp b/src/controllers/taggedusers/TaggedUsersController.hpp deleted file mode 100644 index db9ff2d1..00000000 --- a/src/controllers/taggedusers/TaggedUsersController.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "common/Singleton.hpp" - -#include "common/SignalVector.hpp" -#include "controllers/taggedusers/TaggedUser.hpp" - -namespace chatterino { - -class TaggedUsersModel; - -class TaggedUsersController final : public Singleton -{ -public: - TaggedUsersController(); - - SortedSignalVector> users; - - TaggedUsersModel *createModel(QObject *parent = nullptr); -}; - -} // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 46346589..dfb42904 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,8 @@ #include "common/Args.hpp" #include "common/Modes.hpp" #include "common/Version.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/api/Kraken.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/IncognitoBrowser.hpp" @@ -36,6 +38,9 @@ int main(int argc, char **argv) } else { + Helix::initialize(); + Kraken::initialize(); + Paths *paths{}; try diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index b4222301..4b115af3 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -41,6 +41,14 @@ namespace detail { getApp()->emotes->gifTimer.signal.connect( [this] { this->advance(); }); } + + auto totalLength = std::accumulate( + this->items_.begin(), this->items_.end(), 0UL, + [](auto init, auto &&frame) { return init + frame.duration; }); + + this->durationOffset_ = std::min( + int(getApp()->emotes->gifTimer.position() % totalLength), 60000); + this->processOffset(); } Frames::~Frames() @@ -58,17 +66,16 @@ namespace detail { void Frames::advance() { - this->durationOffset_ += GIF_FRAME_LENGTH; + this->durationOffset_ += gifFrameLength; + this->processOffset(); + } + void Frames::processOffset() + { while (true) { this->index_ %= this->items_.size(); - // TODO: Figure out what this was supposed to achieve - // if (this->index_ >= this->items_.size()) { - // this->index_ = this->index_; - // } - if (this->durationOffset_ > this->items_[this->index_].duration) { this->durationOffset_ -= this->items_[this->index_].duration; @@ -219,10 +226,6 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale) { cache[url] = shared = ImagePtr(new Image(url, scale)); } - else - { - // qDebug() << "same image created multiple times:" << url.string; - } return shared; } diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index f114d1f0..193ba578 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -35,6 +35,7 @@ namespace detail { boost::optional first() const; private: + void processOffset(); QVector> items_; int index_{0}; int durationOffset_{0}; diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 61e8db56..22d1385e 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -125,8 +125,6 @@ public: newChunks->at(0) = newFirstChunk; this->chunks_ = newChunks; - // qDebug() << acceptedItems.size(); - // qDebug() << this->chunks->at(0)->size(); if (this->chunks_->size() == 1) { diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index 218fa3a7..912eeefe 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -35,6 +35,12 @@ SBHighlight Message::getScrollBarHighlight() const return SBHighlight( ColorProvider::instance().color(ColorType::Subscription)); } + else if (this->flags.has(MessageFlag::RedeemedHighlight)) + { + return SBHighlight( + ColorProvider::instance().color(ColorType::RedeemedHighlight), + SBHighlight::Default, true); + } return SBHighlight(); } diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 3b5bb97a..e9456dc8 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -34,6 +34,7 @@ enum class MessageFlag : uint32_t { HighlightedWhisper = (1 << 17), Debug = (1 << 18), Similar = (1 << 19), + RedeemedHighlight = (1 << 20), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 6e4b8115..c215a60a 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/LinkParser.hpp" +#include "controllers/accounts/AccountController.hpp" #include "messages/Image.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" @@ -170,9 +171,12 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, this->message().searchText = text; } +// XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) : MessageBuilder() { + auto current = getApp()->accounts->twitch.getCurrent(); + this->emplace(); this->message().flags.set(MessageFlag::System); this->message().flags.set(MessageFlag::Timeout); @@ -181,43 +185,74 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) QString text; - if (action.isBan()) + if (action.target.id == current->getUserId()) { - if (action.reason.isEmpty()) + text.append("You were "); + if (action.isBan()) { - text = QString("%1 banned %2.") // - .arg(action.source.name) - .arg(action.target.name); + text.append("banned"); } else { - text = QString("%1 banned %2: \"%3\".") // - .arg(action.source.name) - .arg(action.target.name) - .arg(action.reason); + text.append( + QString("timed out for %1").arg(formatTime(action.duration))); + } + + if (!action.source.name.isEmpty()) + { + text.append(" by "); + text.append(action.source.name); + } + + if (action.reason.isEmpty()) + { + text.append("."); + } + else + { + text.append(QString(": \"%1\".").arg(action.reason)); } } else { - if (action.reason.isEmpty()) + if (action.isBan()) { - text = QString("%1 timed out %2 for %3.") // - .arg(action.source.name) - .arg(action.target.name) - .arg(formatTime(action.duration)); + if (action.reason.isEmpty()) + { + text = QString("%1 banned %2.") // + .arg(action.source.name) + .arg(action.target.name); + } + else + { + text = QString("%1 banned %2: \"%3\".") // + .arg(action.source.name) + .arg(action.target.name) + .arg(action.reason); + } } else { - text = QString("%1 timed out %2 for %3: \"%4\".") // - .arg(action.source.name) - .arg(action.target.name) - .arg(formatTime(action.duration)) - .arg(action.reason); - } + if (action.reason.isEmpty()) + { + text = QString("%1 timed out %2 for %3.") // + .arg(action.source.name) + .arg(action.target.name) + .arg(formatTime(action.duration)); + } + else + { + text = QString("%1 timed out %2 for %3: \"%4\".") // + .arg(action.source.name) + .arg(action.target.name) + .arg(formatTime(action.duration)) + .arg(action.reason); + } - if (count > 1) - { - text.append(QString(" (%1 times)").arg(count)); + if (count > 1) + { + text.append(QString(" (%1 times)").arg(count)); + } } } diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 0defed59..d49ca4ad 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,7 +1,6 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" -#include "controllers/moderationactions/ModerationActions.hpp" #include "debug/Benchmark.hpp" #include "messages/Emote.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" @@ -165,21 +164,6 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement( return new ImageLayoutElement(*this, image, size); } -// MOD BADGE -ModBadgeElement::ModBadgeElement(const EmotePtr &data, - MessageElementFlags flags_) - : EmoteElement(data, flags_) -{ -} - -MessageLayoutElement *ModBadgeElement::makeImageLayoutElement( - const ImagePtr &image, const QSize &size) -{ - static const QColor modBadgeBackgroundColor("#34AE0A"); - return new ImageWithBackgroundLayoutElement(*this, image, size, - modBadgeBackgroundColor); -} - // BADGE BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) : MessageElement(flags) @@ -201,8 +185,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container, auto size = QSize(int(container.getScale() * image->width()), int(container.getScale() * image->height())); - container.addElement((new ImageLayoutElement(*this, image, size)) - ->setLink(this->getLink())); + container.addElement(this->makeImageLayoutElement(image, size)); } } @@ -211,6 +194,34 @@ EmotePtr BadgeElement::getEmote() const return this->emote_; } +MessageLayoutElement *BadgeElement::makeImageLayoutElement( + const ImagePtr &image, const QSize &size) +{ + auto element = + (new ImageLayoutElement(*this, image, size))->setLink(this->getLink()); + + return element; +} + +// MOD BADGE +ModBadgeElement::ModBadgeElement(const EmotePtr &data, + MessageElementFlags flags_) + : BadgeElement(data, flags_) +{ +} + +MessageLayoutElement *ModBadgeElement::makeImageLayoutElement( + const ImagePtr &image, const QSize &size) +{ + static const QColor modBadgeBackgroundColor("#34AE0A"); + + auto element = (new ImageWithBackgroundLayoutElement( + *this, image, size, modBadgeBackgroundColor)) + ->setLink(this->getLink()); + + return element; +} + // TEXT TextElement::TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color, FontStyle style) @@ -374,8 +385,8 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, { QSize size(int(container.getScale() * 16), int(container.getScale() * 16)); - - for (const auto &action : getApp()->moderationActions->items) + auto actions = getCSettings().moderationActions.readOnly(); + for (const auto &action : *actions) { if (auto image = action.getImage()) { diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 7d1a00c6..110cf917 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -223,17 +223,6 @@ private: EmotePtr emote_; }; -// Behaves like an emote element, except it creates a different image layout element that draws the mod badge background -class ModBadgeElement : public EmoteElement -{ -public: - ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_); - -protected: - MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, - const QSize &size) override; -}; - class BadgeElement : public MessageElement { public: @@ -244,10 +233,24 @@ public: EmotePtr getEmote() const; +protected: + virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, + const QSize &size); + private: EmotePtr emote_; }; +class ModBadgeElement : public BadgeElement +{ +public: + ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_); + +protected: + MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image, + const QSize &size) override; +}; + // contains a text, formated depending on the preferences class TimestampElement : public MessageElement { diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index c048bd62..e7eaa2ef 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -263,6 +263,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, Selection & /*selection*/) { auto app = getApp(); + auto settings = getSettings(); QPainter painter(buffer); painter.setRenderHint(QPainter::SmoothPixmapTransform); @@ -296,6 +297,14 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, backgroundColor, *ColorProvider::instance().color(ColorType::Subscription)); } + else if (this->message_->flags.has(MessageFlag::RedeemedHighlight) && + settings->enableRedeemedHighlight.getValue()) + { + // Blend highlight color with usual background color + backgroundColor = blendColors( + backgroundColor, + *ColorProvider::instance().color(ColorType::RedeemedHighlight)); + } else if (this->message_->flags.has(MessageFlag::AutoMod)) { backgroundColor = QColor("#404040"); diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp index 492689aa..ad35db05 100644 --- a/src/providers/colors/ColorProvider.cpp +++ b/src/providers/colors/ColorProvider.cpp @@ -1,6 +1,5 @@ #include "providers/colors/ColorProvider.hpp" -#include "controllers/highlights/HighlightController.hpp" #include "singletons/Theme.hpp" namespace chatterino { @@ -38,12 +37,12 @@ QSet ColorProvider::recentColors() const * Currently, only colors used in highlight phrases are considered. This * may change at any point in the future. */ - for (auto phrase : getApp()->highlights->phrases) + for (auto phrase : getSettings()->highlightedMessages) { retVal.insert(*phrase.getColor()); } - for (auto userHl : getApp()->highlights->highlightedUsers) + for (auto userHl : getSettings()->highlightedUsers) { retVal.insert(*userHl.getColor()); } @@ -65,7 +64,6 @@ void ColorProvider::initTypeColorMap() { // Read settings for custom highlight colors and save them in map. // If no custom values can be found, set up default values instead. - auto backgrounds = getApp()->themes->messages.backgrounds; QString customColor = getSettings()->selfHighlightColor; if (QColor(customColor).isValid()) @@ -77,7 +75,8 @@ void ColorProvider::initTypeColorMap() { this->typeColorMap_.insert( {ColorType::SelfHighlight, - std::make_shared(backgrounds.highlighted)}); + std::make_shared( + HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)}); } customColor = getSettings()->subHighlightColor; @@ -90,7 +89,7 @@ void ColorProvider::initTypeColorMap() { this->typeColorMap_.insert( {ColorType::Subscription, - std::make_shared(backgrounds.subscription)}); + std::make_shared(HighlightPhrase::FALLBACK_SUB_COLOR)}); } customColor = getSettings()->whisperHighlightColor; @@ -103,22 +102,41 @@ void ColorProvider::initTypeColorMap() { this->typeColorMap_.insert( {ColorType::Whisper, - std::make_shared(backgrounds.highlighted)}); + std::make_shared( + HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)}); + } + + customColor = getSettings()->redeemedHighlightColor; + if (QColor(customColor).isValid()) + { + this->typeColorMap_.insert({ColorType::RedeemedHighlight, + std::make_shared(customColor)}); + } + else + { + this->typeColorMap_.insert( + {ColorType::RedeemedHighlight, + std::make_shared( + HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR)}); } } void ColorProvider::initDefaultColors() { // Init default colors - this->defaultColors_.emplace_back(31, 141, 43, 127); // Green-ish - this->defaultColors_.emplace_back(28, 126, 141, 127); // Blue-ish - this->defaultColors_.emplace_back(136, 141, 49, 127); // Golden-ish - this->defaultColors_.emplace_back(143, 48, 24, 127); // Red-ish - this->defaultColors_.emplace_back(28, 141, 117, 127); // Cyan-ish + this->defaultColors_.emplace_back(75, 127, 107, 100); // Teal + this->defaultColors_.emplace_back(105, 127, 63, 100); // Olive + this->defaultColors_.emplace_back(63, 83, 127, 100); // Blue + this->defaultColors_.emplace_back(72, 127, 63, 100); // Green - auto backgrounds = getApp()->themes->messages.backgrounds; - this->defaultColors_.push_back(backgrounds.highlighted); - this->defaultColors_.push_back(backgrounds.subscription); + this->defaultColors_.emplace_back(31, 141, 43, 115); // Green + this->defaultColors_.emplace_back(28, 126, 141, 90); // Blue + this->defaultColors_.emplace_back(136, 141, 49, 90); // Golden + this->defaultColors_.emplace_back(143, 48, 24, 127); // Red + this->defaultColors_.emplace_back(28, 141, 117, 90); // Cyan + + this->defaultColors_.push_back(HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR); + this->defaultColors_.push_back(HighlightPhrase::FALLBACK_SUB_COLOR); } } // namespace chatterino diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp index e038b17f..1595c53d 100644 --- a/src/providers/colors/ColorProvider.hpp +++ b/src/providers/colors/ColorProvider.hpp @@ -2,7 +2,12 @@ namespace chatterino { -enum class ColorType { SelfHighlight, Subscription, Whisper }; +enum class ColorType { + SelfHighlight, + Subscription, + Whisper, + RedeemedHighlight +}; class ColorProvider { diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 29dbac52..8589e3a6 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -102,7 +102,7 @@ namespace { modBadge = std::make_shared(Emote{ {""}, modBadgeImageSet, - Tooltip{"Twitch Channel Moderator"}, + Tooltip{"Moderator"}, modBadge1x, }); } diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 0a3019e5..2618465a 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -71,8 +71,30 @@ AbstractIrcServer::AbstractIrcServer() }); } +void AbstractIrcServer::initializeIrc() +{ + assert(!this->initialized_); + + if (this->hasSeparateWriteConnection()) + { + this->initializeConnectionSignals(this->writeConnection_.get(), + ConnectionType::Write); + this->initializeConnectionSignals(this->readConnection_.get(), + ConnectionType::Read); + } + else + { + this->initializeConnectionSignals(this->readConnection_.get(), + ConnectionType::Both); + } + + this->initialized_ = true; +} + void AbstractIrcServer::connect() { + assert(this->initialized_); + this->disconnect(); if (this->hasSeparateWriteConnection()) diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 20d78249..1c865ef5 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -21,6 +21,10 @@ public: virtual ~AbstractIrcServer() = default; + // initializeIrc must be called from the derived class + // this allows us to initialize the abstract irc server based on the derived class's parameters + void initializeIrc(); + // connection void connect(); void disconnect(); @@ -45,8 +49,15 @@ public: protected: AbstractIrcServer(); + // initializeConnectionSignals is called on a connection once in its lifetime. + // it can be used to connect signals to your class + virtual void initializeConnectionSignals(IrcConnection *connection, + ConnectionType type){}; + + // initializeConnection is called every time before we try to connect to the irc server virtual void initializeConnection(IrcConnection *connection, ConnectionType type) = 0; + virtual std::shared_ptr createChannel( const QString &channelName) = 0; @@ -83,6 +94,8 @@ private: // bool autoReconnect_ = false; pajlada::Signals::SignalHolder connections_; + + bool initialized_{false}; }; } // namespace chatterino diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp index 0a8f2ed6..2f685be6 100644 --- a/src/providers/irc/Irc2.cpp +++ b/src/providers/irc/Irc2.cpp @@ -143,7 +143,7 @@ Irc::Irc() QAbstractTableModel *Irc::newConnectionModel(QObject *parent) { auto model = new Model(parent); - model->init(&this->connections); + model->initialize(&this->connections); return model; } @@ -252,7 +252,7 @@ void Irc::load() { ids.insert(data.id); - this->connections.appendItem(data); + this->connections.append(data); } } } diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp index de484924..0cdc60c7 100644 --- a/src/providers/irc/Irc2.hpp +++ b/src/providers/irc/Irc2.hpp @@ -41,7 +41,7 @@ public: static inline void *const noEraseCredentialCaller = reinterpret_cast(1); - UnsortedSignalVector connections; + SignalVector connections; QAbstractTableModel *newConnectionModel(QObject *parent); ChannelPtr getOrAddChannel(int serverId, QString name); diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index dd7505ff..1b3aea5f 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -15,6 +15,8 @@ namespace chatterino { IrcServer::IrcServer(const IrcServerData &data) : data_(new IrcServerData(data)) { + this->initializeIrc(); + this->connect(); } @@ -51,6 +53,57 @@ const QString &IrcServer::nick() return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick; } +void IrcServer::initializeConnectionSignals(IrcConnection *connection, + ConnectionType type) +{ + assert(type == Both); + + QObject::connect( + connection, &Communi::IrcConnection::socketError, this, + [this](QAbstractSocket::SocketError error) { + static int index = + QAbstractSocket::staticMetaObject.indexOfEnumerator( + "SocketError"); + + std::lock_guard lock(this->channelMutex); + + for (auto &&weak : this->channels) + { + if (auto shared = weak.lock()) + { + shared->addMessage(makeSystemMessage( + QStringLiteral("Socket error: ") + + QAbstractSocket::staticMetaObject.enumerator(index) + .valueToKey(error))); + } + } + }); + + QObject::connect(connection, &Communi::IrcConnection::nickNameRequired, + this, [](const QString &reserved, QString *result) { + *result = reserved + (std::rand() % 100); + }); + + QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived, + this, [this](Communi::IrcNoticeMessage *message) { + MessageBuilder builder; + + builder.emplace(); + builder.emplace( + message->nick(), MessageElementFlag::Username); + builder.emplace( + "-> you:", MessageElementFlag::Username); + builder.emplace(message->content(), + MessageElementFlag::Text); + + auto msg = builder.release(); + + for (auto &&weak : this->channels) + if (auto shared = weak.lock()) + shared->addMessage(msg); + }); +} + void IrcServer::initializeConnection(IrcConnection *connection, ConnectionType type) { @@ -90,47 +143,6 @@ void IrcServer::initializeConnection(IrcConnection *connection, this->open(Both); } } - - QObject::connect( - connection, &Communi::IrcConnection::socketError, this, - [this](QAbstractSocket::SocketError error) { - static int index = - QAbstractSocket::staticMetaObject.indexOfEnumerator( - "SocketError"); - - std::lock_guard lock(this->channelMutex); - - for (auto &&weak : this->channels) - if (auto shared = weak.lock()) - shared->addMessage(makeSystemMessage( - QStringLiteral("Socket error: ") + - QAbstractSocket::staticMetaObject.enumerator(index) - .valueToKey(error))); - }); - - QObject::connect(connection, &Communi::IrcConnection::nickNameRequired, - this, [](const QString &reserved, QString *result) { - *result = reserved + (std::rand() % 100); - }); - - QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived, - this, [this](Communi::IrcNoticeMessage *message) { - MessageBuilder builder; - - builder.emplace(); - builder.emplace( - message->nick(), MessageElementFlag::Username); - builder.emplace( - "-> you:", MessageElementFlag::Username); - builder.emplace(message->content(), - MessageElementFlag::Text); - - auto msg = builder.release(); - - for (auto &&weak : this->channels) - if (auto shared = weak.lock()) - shared->addMessage(msg); - }); } std::shared_ptr IrcServer::createChannel(const QString &channelName) diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp index 0b5a0aad..6c1251fa 100644 --- a/src/providers/irc/IrcServer.hpp +++ b/src/providers/irc/IrcServer.hpp @@ -21,6 +21,8 @@ public: // AbstractIrcServer interface protected: + void initializeConnectionSignals(IrcConnection *connection, + ConnectionType type) override; void initializeConnection(IrcConnection *connection, ConnectionType type) override; std::shared_ptr createChannel(const QString &channelName) override; diff --git a/src/providers/twitch/ChatroomChannel.cpp b/src/providers/twitch/ChatroomChannel.cpp deleted file mode 100644 index 5f393e42..00000000 --- a/src/providers/twitch/ChatroomChannel.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "ChatroomChannel.hpp" - -#include -#include "TwitchApi.hpp" -#include "common/Common.hpp" -#include "messages/Emote.hpp" -#include "providers/bttv/BttvEmotes.hpp" -#include "providers/bttv/LoadBttvChannelEmote.hpp" -#include "singletons/Emotes.hpp" - -namespace chatterino { - -ChatroomChannel::ChatroomChannel(const QString &channelName, - TwitchBadges &globalTwitchBadges, - BttvEmotes &globalBttv, FfzEmotes &globalFfz) - : TwitchChannel(channelName, globalTwitchBadges, globalBttv, globalFfz) -{ - auto listRef = channelName.splitRef(":"); - if (listRef.size() > 2) - { - this->chatroomOwnerId = listRef[1].toString(); - } -} - -void ChatroomChannel::refreshBTTVChannelEmotes() -{ - if (this->chatroomOwnerId.isEmpty()) - { - return; - } - TwitchApi::findUserName( - this->chatroomOwnerId, - [this, weak = weakOf(this)](QString username) { - BttvEmotes::loadChannel(username, [this, weak](auto &&emoteMap) { - if (auto shared = weak.lock()) - this->bttvEmotes_.set( - std::make_shared(std::move(emoteMap))); - }); - if (auto shared = weak.lock()) - { - this->chatroomOwnerName = username; - } - }); -} -void ChatroomChannel::refreshFFZChannelEmotes() -{ - if (this->chatroomOwnerId.isEmpty()) - { - return; - } - FfzEmotes::loadChannel( - this->chatroomOwnerId, - [this](auto &&emoteMap) { - this->ffzEmotes_.set( - std::make_shared(std::move(emoteMap))); - }, - [this](auto &&modBadge) { - this->ffzCustomModBadge_.set(std::move(modBadge)); - }); -} - -const QString &ChatroomChannel::getDisplayName() const -{ - return this->chatroomOwnerName; -} - -} // namespace chatterino diff --git a/src/providers/twitch/ChatroomChannel.hpp b/src/providers/twitch/ChatroomChannel.hpp deleted file mode 100644 index 2ed24914..00000000 --- a/src/providers/twitch/ChatroomChannel.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "TwitchChannel.hpp" - -#include -#include - -namespace chatterino { - -class ChatroomChannel : public TwitchChannel -{ -protected: - explicit ChatroomChannel(const QString &channelName, - TwitchBadges &globalTwitchBadges, - BttvEmotes &globalBttv, FfzEmotes &globalFfz); - virtual void refreshBTTVChannelEmotes() override; - virtual void refreshFFZChannelEmotes() override; - virtual const QString &getDisplayName() const override; - - QString chatroomOwnerId; - QString chatroomOwnerName; - - friend class TwitchIrcServer; - friend class TwitchMessageBuilder; - friend class IrcMessageHandler; -}; - -} // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7a456eca..833586d4 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -2,7 +2,6 @@ #include "Application.hpp" #include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" #include "messages/LimitedQueue.hpp" #include "messages/Message.hpp" #include "providers/twitch/TwitchAccountManager.hpp" @@ -241,7 +240,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, if (highlighted) { server.mentionsChannel->addMessage(msg); - getApp()->highlights->addHighlight(msg); } } @@ -453,7 +451,6 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) { auto app = getApp(); - qDebug() << "Received whisper!"; MessageParseArgs args; args.isReceivedWhisper = true; diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp deleted file mode 100644 index 5407fbad..00000000 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "providers/twitch/PartialTwitchUser.hpp" - -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "providers/twitch/TwitchCommon.hpp" - -#include -#include - -namespace chatterino { - -PartialTwitchUser PartialTwitchUser::byName(const QString &username) -{ - PartialTwitchUser user; - user.username_ = username; - - return user; -} - -PartialTwitchUser PartialTwitchUser::byId(const QString &id) -{ - PartialTwitchUser user; - user.id_ = id; - - return user; -} - -void PartialTwitchUser::getId(std::function successCallback, - const QObject *caller) -{ - getId( - successCallback, [] {}, caller); -} -void PartialTwitchUser::getId(std::function successCallback, - std::function failureCallback, - const QObject *caller) -{ - assert(!this->username_.isEmpty()); - - NetworkRequest("https://api.twitch.tv/kraken/users?login=" + - this->username_) - .caller(caller) - .authorizeTwitchV5(getDefaultClientID()) - .onSuccess([successCallback, failureCallback](auto result) -> Outcome { - auto root = result.parseJson(); - if (!root.value("users").isArray()) - { - qDebug() - << "API Error while getting user id, users is not an array"; - failureCallback(); - return Failure; - } - - auto users = root.value("users").toArray(); - if (users.size() != 1) - { - qDebug() << "API Error while getting user id, users array size " - "is not 1"; - failureCallback(); - return Failure; - } - if (!users[0].isObject()) - { - qDebug() << "API Error while getting user id, first user is " - "not an object"; - failureCallback(); - return Failure; - } - auto firstUser = users[0].toObject(); - auto id = firstUser.value("_id"); - if (!id.isString()) - { - qDebug() << "API Error: while getting user id, first user " - "object `_id` key is not a string"; - failureCallback(); - return Failure; - } - successCallback(id.toString()); - - return Success; - }) - .execute(); -} - -} // namespace chatterino diff --git a/src/providers/twitch/PartialTwitchUser.hpp b/src/providers/twitch/PartialTwitchUser.hpp deleted file mode 100644 index 6537bb27..00000000 --- a/src/providers/twitch/PartialTwitchUser.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include - -namespace chatterino { - -// Experimental class to test a method of calling APIs on twitch users -class PartialTwitchUser -{ - PartialTwitchUser() = default; - - QString username_; - QString id_; - -public: - static PartialTwitchUser byName(const QString &username); - static PartialTwitchUser byId(const QString &id); - - void getId(std::function successCallback, - const QObject *caller = nullptr); - - void getId(std::function successCallback, - std::function failureCallback, - const QObject *caller = nullptr); -}; - -} // namespace chatterino diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 9cf57c28..c48d02c5 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -820,14 +820,12 @@ void PubSub::listen(rapidjson::Document &&msg) this->addClient(); - qDebug() << "Added to the back of the queue"; this->requests.emplace_back( std::make_unique(std::move(msg))); } bool PubSub::tryListen(rapidjson::Document &msg) { - qDebug() << "tryListen with" << this->clients.size() << "clients"; for (const auto &p : this->clients) { const auto &client = p.second; @@ -1064,7 +1062,6 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) else { qDebug() << "Invalid whisper type:" << whisperType.c_str(); - assert(false); return; } } diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 13b4d409..9ece767a 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -6,8 +6,8 @@ #include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" -#include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "providers/twitch/api/Helix.hpp" #include "singletons/Emotes.hpp" #include "util/RapidjsonHelpers.hpp" @@ -151,12 +151,14 @@ void TwitchAccount::ignore( const QString &targetName, std::function onFinished) { - const auto onIdFetched = [this, targetName, - onFinished](QString targetUserId) { - this->ignoreByID(targetUserId, targetName, onFinished); // + const auto onUserFetched = [this, targetName, + onFinished](const auto &user) { + this->ignoreByID(user.id, targetName, onFinished); // }; - PartialTwitchUser::byName(targetName).getId(onIdFetched); + const auto onUserFetchFailed = [] {}; + + getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed); } void TwitchAccount::ignoreByID( @@ -226,12 +228,14 @@ void TwitchAccount::unignore( const QString &targetName, std::function onFinished) { - const auto onIdFetched = [this, targetName, - onFinished](QString targetUserId) { - this->unignoreByID(targetUserId, targetName, onFinished); // + const auto onUserFetched = [this, targetName, + onFinished](const auto &user) { + this->unignoreByID(user.id, targetName, onFinished); // }; - PartialTwitchUser::byName(targetName).getId(onIdFetched); + const auto onUserFetchFailed = [] {}; + + getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed); } void TwitchAccount::unignoreByID( @@ -270,28 +274,18 @@ void TwitchAccount::unignoreByID( void TwitchAccount::checkFollow(const QString targetUserID, std::function onFinished) { - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + - "/follows/channels/" + targetUserID); + const auto onResponse = [onFinished](bool following, const auto &record) { + if (!following) + { + onFinished(FollowResult_NotFollowing); + return; + } - NetworkRequest(url) + onFinished(FollowResult_Following); + }; - .authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken()) - .onError([=](NetworkResult result) { - if (result.status() == 203) - { - onFinished(FollowResult_NotFollowing); - } - else - { - onFinished(FollowResult_Failed); - } - }) - .onSuccess([=](auto result) -> Outcome { - auto document = result.parseRapidJson(); - onFinished(FollowResult_Following); - return Success; - }) - .execute(); + getHelix()->getUserFollow(this->getUserId(), targetUserID, onResponse, + [] {}); } void TwitchAccount::followUser(const QString userID, @@ -392,7 +386,6 @@ void TwitchAccount::autoModAllow(const QString msgID) QString url("https://api.twitch.tv/kraken/chat/twitchbot/approve"); auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8(); - qDebug() << qba; NetworkRequest(url, NetworkRequestType::Post) .header("Content-Type", "application/json") @@ -412,7 +405,6 @@ void TwitchAccount::autoModDeny(const QString msgID) QString url("https://api.twitch.tv/kraken/chat/twitchbot/deny"); auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8(); - qDebug() << qba; NetworkRequest(url, NetworkRequestType::Post) .header("Content-Type", "application/json") diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index b772f291..3813e982 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -3,11 +3,14 @@ #include "common/Common.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/api/Kraken.hpp" namespace chatterino { TwitchAccountManager::TwitchAccountManager() - : anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", "")) + : accounts(SharedPtrElementLess{}) + , anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", "")) { this->currentUserChanged.connect([this] { auto currentUser = this->getCurrent(); @@ -140,6 +143,8 @@ void TwitchAccountManager::load() if (user) { qDebug() << "Twitch user updated to" << newUsername; + getHelix()->update(user->getOAuthClient(), user->getOAuthToken()); + getKraken()->update(user->getOAuthClient(), user->getOAuthToken()); this->currentUser_ = user; } else @@ -221,7 +226,7 @@ TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser( // std::lock_guard lock(this->mutex); - this->accounts.insertItem(newUser); + this->accounts.insert(newUser); return AddUserResponse::UserAdded; } diff --git a/src/providers/twitch/TwitchAccountManager.hpp b/src/providers/twitch/TwitchAccountManager.hpp index c9ce0215..5956fdf0 100644 --- a/src/providers/twitch/TwitchAccountManager.hpp +++ b/src/providers/twitch/TwitchAccountManager.hpp @@ -51,9 +51,7 @@ public: pajlada::Signals::NoArgSignal currentUserChanged; pajlada::Signals::NoArgSignal userListUpdated; - SortedSignalVector, - SharedPtrElementLess> - accounts; + SignalVector> accounts; private: enum class AddUserResponse { diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp deleted file mode 100644 index 6c3b6b6e..00000000 --- a/src/providers/twitch/TwitchApi.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "providers/twitch/TwitchApi.hpp" - -#include "common/Common.hpp" -#include "common/NetworkRequest.hpp" -#include "providers/twitch/TwitchCommon.hpp" - -#include -#include - -namespace chatterino { - -void TwitchApi::findUserId(const QString user, - std::function successCallback) -{ - QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user); - - NetworkRequest(requestUrl) - - .authorizeTwitchV5(getDefaultClientID()) - .timeout(30000) - .onSuccess([successCallback](auto result) mutable -> Outcome { - auto root = result.parseJson(); - if (!root.value("users").isArray()) - { - qDebug() - << "API Error while getting user id, users is not an array"; - successCallback(""); - return Failure; - } - auto users = root.value("users").toArray(); - if (users.size() != 1) - { - qDebug() << "API Error while getting user id, users array size " - "is not 1"; - successCallback(""); - return Failure; - } - if (!users[0].isObject()) - { - qDebug() << "API Error while getting user id, first user is " - "not an object"; - successCallback(""); - return Failure; - } - auto firstUser = users[0].toObject(); - auto id = firstUser.value("_id"); - if (!id.isString()) - { - qDebug() << "API Error: while getting user id, first user " - "object `_id` key is not a string"; - successCallback(""); - return Failure; - } - successCallback(id.toString()); - return Success; - }) - .execute(); -} - -void TwitchApi::findUserName(const QString userid, - std::function successCallback) -{ - QString requestUrl("https://api.twitch.tv/kraken/users/" + userid); - - NetworkRequest(requestUrl) - - .authorizeTwitchV5(getDefaultClientID()) - .timeout(30000) - .onSuccess([successCallback](auto result) mutable -> Outcome { - auto root = result.parseJson(); - auto name = root.value("name"); - if (!name.isString()) - { - qDebug() << "API Error: while getting user name, `name` is not " - "a string"; - successCallback(""); - return Failure; - } - successCallback(name.toString()); - return Success; - }) - .execute(); -} - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchApi.hpp b/src/providers/twitch/TwitchApi.hpp deleted file mode 100644 index 8b1b85c1..00000000 --- a/src/providers/twitch/TwitchApi.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -namespace chatterino { - -class TwitchApi -{ -public: - static void findUserId(const QString user, - std::function callback); - static void findUserName(const QString userid, - std::function callback); - -private: -}; - -} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index d305f0c4..ac448b30 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -14,6 +14,8 @@ #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchParseCheerEmotes.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/api/Kraken.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/Toasts.hpp" @@ -21,6 +23,7 @@ #include "util/PostToThread.hpp" #include "widgets/Window.hpp" +#include #include #include #include @@ -454,35 +457,25 @@ void TwitchChannel::refreshTitle() } this->titleRefreshedTime_ = QTime::currentTime(); - QString url("https://api.twitch.tv/kraken/channels/" + roomID); - NetworkRequest::twitchRequest(url) - .onSuccess( - [this, weak = weakOf(this)](auto result) -> Outcome { - ChannelPtr shared = weak.lock(); - if (!shared) - return Failure; + const auto onSuccess = [this, + weak = weakOf(this)](const auto &channel) { + ChannelPtr shared = weak.lock(); + if (!shared) + { + return; + } - const auto document = result.parseRapidJson(); + { + auto status = this->streamStatus_.access(); + status->title = channel.status; + } - auto statusIt = document.FindMember("status"); + this->liveStatusChanged.invoke(); + }; - if (statusIt == document.MemberEnd()) - { - return Failure; - } + const auto onFailure = [] {}; - { - auto status = this->streamStatus_.access(); - if (!rj::getSafe(statusIt->value, status->title)) - { - return Failure; - } - } - - this->liveStatusChanged.invoke(); - return Success; - }) - .execute(); + getKraken()->getChannel(roomID, onSuccess, onFailure); } void TwitchChannel::refreshLiveStatus() @@ -497,106 +490,72 @@ void TwitchChannel::refreshLiveStatus() return; } - QString url("https://api.twitch.tv/kraken/streams/" + roomID); + getHelix()->getStreamById( + roomID, + [this, weak = weakOf(this)](bool live, const auto &stream) { + ChannelPtr shared = weak.lock(); + if (!shared) + { + return; + } - // auto request = makeGetStreamRequest(roomID, QThread::currentThread()); - NetworkRequest::twitchRequest(url) - - .onSuccess( - [this, weak = weakOf(this)](auto result) -> Outcome { - ChannelPtr shared = weak.lock(); - if (!shared) - return Failure; - - return this->parseLiveStatus(result.parseRapidJson()); - }) - .execute(); + this->parseLiveStatus(live, stream); + }, + [] { + // failure + }); } -Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) +void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream) { - if (!document.IsObject()) + if (!live) { - qDebug() << "[TwitchChannel:refreshLiveStatus] root is not an object"; - return Failure; - } - - if (!document.HasMember("stream")) - { - qDebug() << "[TwitchChannel:refreshLiveStatus] Missing stream in root"; - return Failure; - } - - const auto &stream = document["stream"]; - - if (!stream.IsObject()) - { - // Stream is offline (stream is most likely null) this->setLive(false); - return Failure; + return; } - if (!stream.HasMember("viewers") || !stream.HasMember("game") || - !stream.HasMember("channel") || !stream.HasMember("created_at")) - { - qDebug() - << "[TwitchChannel:refreshLiveStatus] Missing members in stream"; - this->setLive(false); - return Failure; - } - - const rapidjson::Value &streamChannel = stream["channel"]; - - if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) - { - qDebug() << "[TwitchChannel:refreshLiveStatus] Missing member " - "\"status\" in channel"; - return Failure; - } - - // Stream is live - { auto status = this->streamStatus_.access(); - status->viewerCount = stream["viewers"].GetUint(); - status->game = stream["game"].GetString(); - status->title = streamChannel["status"].GetString(); - QDateTime since = QDateTime::fromString( - stream["created_at"].GetString(), Qt::ISODate); + status->viewerCount = stream.viewerCount; + if (status->gameId != stream.gameId) + { + status->gameId = stream.gameId; + + // Resolve game ID to game name + getHelix()->getGameById( + stream.gameId, + [this, weak = weakOf(this)](const auto &game) { + ChannelPtr shared = weak.lock(); + if (!shared) + { + return; + } + + { + auto status = this->streamStatus_.access(); + status->game = game.name; + } + + this->liveStatusChanged.invoke(); + }, + [] { + // failure + }); + } + status->title = stream.title; + QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate); auto diff = since.secsTo(QDateTime::currentDateTime()); status->uptime = QString::number(diff / 3600) + "h " + QString::number(diff % 3600 / 60) + "m"; status->rerun = false; - if (stream.HasMember("stream_type")) - { - status->streamType = stream["stream_type"].GetString(); - } - else - { - status->streamType = QString(); - } - - if (stream.HasMember("broadcast_platform")) - { - const auto &broadcastPlatformValue = stream["broadcast_platform"]; - - if (broadcastPlatformValue.IsString()) - { - const char *broadcastPlatform = - stream["broadcast_platform"].GetString(); - if (strcmp(broadcastPlatform, "rerun") == 0) - { - status->rerun = true; - } - } - } + status->streamType = stream.type; } - setLive(true); + + this->setLive(true); + // Signal all listeners that the stream status has been updated this->liveStatusChanged.invoke(); - - return Success; } void TwitchChannel::loadRecentMessages() @@ -641,9 +600,6 @@ void TwitchChannel::loadRecentMessages() void TwitchChannel::refreshPubsub() { - // listen to moderation actions - if (!this->hasModRights()) - return; auto roomId = this->roomId(); if (roomId.isEmpty()) return; diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 026c9071..fc40ad32 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -8,14 +8,15 @@ #include "common/UniqueAccess.hpp" #include "common/UsernameSet.hpp" #include "providers/twitch/TwitchEmotes.hpp" +#include "providers/twitch/api/Helix.hpp" -#include #include #include #include #include -#include #include + +#include #include namespace chatterino { @@ -43,6 +44,7 @@ public: unsigned viewerCount = 0; QString title; QString game; + QString gameId; QString uptime; QString streamType; }; @@ -120,7 +122,7 @@ protected: private: // Methods void refreshLiveStatus(); - Outcome parseLiveStatus(const rapidjson::Document &document); + void parseLiveStatus(bool live, const HelixStream &stream); void refreshPubsub(); void refreshChatters(); void refreshBadges(); diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 45f50cec..0f1d4884 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -7,10 +7,8 @@ #include "common/Common.hpp" #include "common/Env.hpp" #include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" -#include "providers/twitch/ChatroomChannel.hpp" #include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchAccount.hpp" @@ -24,26 +22,13 @@ using namespace std::chrono_literals; namespace chatterino { -namespace { - bool isChatroom(const QString &channel) - { - if (channel.left(10) == "chatrooms:") - { - auto reflist = channel.splitRef(':'); - if (reflist.size() == 3) - { - return true; - } - } - return false; - } -} // namespace - TwitchIrcServer::TwitchIrcServer() : whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers)) , mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions)) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) { + this->initializeIrc(); + this->pubsub = new PubSub; // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { @@ -100,18 +85,8 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection, std::shared_ptr TwitchIrcServer::createChannel( const QString &channelName) { - std::shared_ptr channel; - if (isChatroom(channelName)) - { - channel = std::static_pointer_cast( - std::shared_ptr(new ChatroomChannel( - channelName, this->twitchBadges, this->bttv, this->ffz))); - } - else - { - channel = std::shared_ptr(new TwitchChannel( - channelName, this->twitchBadges, this->bttv, this->ffz)); - } + auto channel = std::shared_ptr(new TwitchChannel( + channelName, this->twitchBadges, this->bttv, this->ffz)); channel->initialize(); channel->sendMessageSignal.connect( diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 0d468047..02a45a8f 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -2,9 +2,8 @@ #include "Application.hpp" #include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" #include "controllers/ignores/IgnoreController.hpp" -#include "controllers/pings/PingController.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" #include "messages/Message.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/twitch/TwitchBadges.hpp" @@ -28,7 +27,8 @@ namespace { const QSet zeroWidthEmotes{ - "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", + "SoSnowy", "IceCold", "SantaHat", "TopHat", + "ReinDeer", "CandyCane", "cvMask", "cvHazmat", }; QColor getRandomColor(const QVariant &userId) @@ -170,12 +170,11 @@ bool TwitchMessageBuilder::isIgnored() const auto app = getApp(); // TODO(pajlada): Do we need to check if the phrase is valid first? - for (const auto &phrase : app->ignores->phrases) + auto phrases = getCSettings().ignoredMessages.readOnly(); + for (const auto &phrase : *phrases) { if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) { - qDebug() << "Blocking message because it contains ignored phrase" - << phrase.getPattern(); return true; } } @@ -205,8 +204,7 @@ bool TwitchMessageBuilder::isIgnored() const case ShowIgnoredUsersMessages::Never: break; } - qDebug() << "Blocking message because it's from blocked user" - << user.name; + return true; } } @@ -238,7 +236,7 @@ void TwitchMessageBuilder::triggerHighlights() return; } - if (getApp()->pings->isMuted(this->channel->getName())) + if (getCSettings().isMutedChannel(this->channel->getName())) { // Do nothing. Pings are muted in this channel. return; @@ -297,6 +295,13 @@ MessagePtr TwitchMessageBuilder::build() this->historicalMessage_ = this->tags.contains("historical"); + if (this->tags.contains("msg-id") && + this->tags["msg-id"].toString().split(';').contains( + "highlighted-message")) + { + this->message().flags.set(MessageFlag::RedeemedHighlight); + } + // timestamp if (this->historicalMessage_) { @@ -764,8 +769,7 @@ void TwitchMessageBuilder::appendUsername() void TwitchMessageBuilder::runIgnoreReplaces( std::vector> &twitchEmotes) { - auto app = getApp(); - const auto &phrases = app->ignores->phrases; + auto phrases = getCSettings().ignoredMessages.readOnly(); auto removeEmotesInRange = [](int pos, int len, std::vector> @@ -828,7 +832,7 @@ void TwitchMessageBuilder::runIgnoreReplaces( } }; - for (const auto &phrase : phrases) + for (const auto &phrase : *phrases) { if (phrase.isBlock()) { @@ -1017,7 +1021,7 @@ void TwitchMessageBuilder::parseHighlights() QString currentUsername = currentUser->getUserName(); - if (app->highlights->blacklistContains(this->ircMessage->nick())) + if (getCSettings().isBlacklistedUser(this->ircMessage->nick())) { // Do nothing. We ignore highlights from this user. return; @@ -1056,18 +1060,14 @@ void TwitchMessageBuilder::parseHighlights() */ } - std::vector userHighlights = - app->highlights->highlightedUsers.cloneVector(); - // Highlight because of sender - for (const HighlightPhrase &userHighlight : userHighlights) + auto userHighlights = getCSettings().highlightedUsers.readOnly(); + for (const HighlightPhrase &userHighlight : *userHighlights) { if (!userHighlight.isMatch(this->ircMessage->nick())) { continue; } - qDebug() << "Highlight because user" << this->ircMessage->nick() - << "sent a message"; this->message().flags.set(MessageFlag::Highlighted); this->message().highlightColor = userHighlight.getColor(); @@ -1111,7 +1111,7 @@ void TwitchMessageBuilder::parseHighlights() // TODO: This vector should only be rebuilt upon highlights being changed // fourtf: should be implemented in the HighlightsController std::vector activeHighlights = - app->highlights->phrases.cloneVector(); + getSettings()->highlightedMessages.cloneVector(); if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) { @@ -1131,9 +1131,6 @@ void TwitchMessageBuilder::parseHighlights() continue; } - qDebug() << "Highlight because" << this->originalMessage_ << "matches" - << highlight.getPattern(); - this->message().flags.set(MessageFlag::Highlighted); this->message().highlightColor = highlight.getColor(); diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp index 5258baf9..9667ffd2 100644 --- a/src/providers/twitch/TwitchParseCheerEmotes.cpp +++ b/src/providers/twitch/TwitchParseCheerEmotes.cpp @@ -278,6 +278,7 @@ namespace { return true; } + } // namespace // Look through the results of diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp new file mode 100644 index 00000000..974b6212 --- /dev/null +++ b/src/providers/twitch/api/Helix.cpp @@ -0,0 +1,368 @@ +#include "providers/twitch/api/Helix.hpp" + +#include "common/Outcome.hpp" + +namespace chatterino { + +static Helix *instance = nullptr; + +void Helix::fetchUsers(QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + + for (const auto &id : userIds) + { + urlQuery.addQueryItem("id", id); + } + + for (const auto &login : userLogins) + { + urlQuery.addQueryItem("login", login); + } + + // TODO: set on success and on error + this->makeRequest("users", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector users; + + for (const auto &jsonUser : data.toArray()) + { + users.emplace_back(jsonUser.toObject()); + } + + successCallback(users); + + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::getUserByName(QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QStringList userIds; + QStringList userLogins{userId}; + + this->fetchUsers( + userIds, userLogins, + [successCallback, + failureCallback](const std::vector &users) { + if (users.empty()) + { + failureCallback(); + return; + } + successCallback(users[0]); + }, + failureCallback); +} + +void Helix::getUserById(QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QStringList userIds{userId}; + QStringList userLogins; + + this->fetchUsers( + userIds, userLogins, + [successCallback, failureCallback](const auto &users) { + if (users.empty()) + { + failureCallback(); + return; + } + successCallback(users[0]); + }, + failureCallback); +} + +void Helix::fetchUsersFollows( + QString fromId, QString toId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + assert(!fromId.isEmpty() || !toId.isEmpty()); + + QUrlQuery urlQuery; + + if (!fromId.isEmpty()) + { + urlQuery.addQueryItem("from_id", fromId); + } + + if (!toId.isEmpty()) + { + urlQuery.addQueryItem("to_id", toId); + } + + // TODO: set on success and on error + this->makeRequest("users/follows", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + if (root.empty()) + { + failureCallback(); + return Failure; + } + successCallback(HelixUsersFollowsResponse(root)); + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::getUserFollowers( + QString userId, ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + this->fetchUsersFollows("", userId, successCallback, failureCallback); +} + +void Helix::getUserFollow( + QString userId, QString targetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + this->fetchUsersFollows( + userId, targetId, + [successCallback](const auto &response) { + if (response.data.empty()) + { + successCallback(false, HelixUsersFollowsRecord()); + return; + } + + successCallback(true, response.data[0]); + }, + failureCallback); +} + +void Helix::fetchStreams( + QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + QUrlQuery urlQuery; + + for (const auto &id : userIds) + { + urlQuery.addQueryItem("user_id", id); + } + + for (const auto &login : userLogins) + { + urlQuery.addQueryItem("user_login", login); + } + + // TODO: set on success and on error + this->makeRequest("streams", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector streams; + + for (const auto &jsonStream : data.toArray()) + { + streams.emplace_back(jsonStream.toObject()); + } + + successCallback(streams); + + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::getStreamById(QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QStringList userIds{userId}; + QStringList userLogins; + + this->fetchStreams( + userIds, userLogins, + [successCallback, failureCallback](const auto &streams) { + if (streams.empty()) + { + successCallback(false, HelixStream()); + return; + } + successCallback(true, streams[0]); + }, + failureCallback); +} + +void Helix::getStreamByName(QString userName, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QStringList userIds; + QStringList userLogins{userName}; + + this->fetchStreams( + userIds, userLogins, + [successCallback, failureCallback](const auto &streams) { + if (streams.empty()) + { + successCallback(false, HelixStream()); + return; + } + successCallback(true, streams[0]); + }, + failureCallback); +} + +/// + +void Helix::fetchGames(QStringList gameIds, QStringList gameNames, + ResultCallback> successCallback, + HelixFailureCallback failureCallback) +{ + assert((gameIds.length() + gameNames.length()) > 0); + + QUrlQuery urlQuery; + + for (const auto &id : gameIds) + { + urlQuery.addQueryItem("id", id); + } + + for (const auto &login : gameNames) + { + urlQuery.addQueryItem("name", login); + } + + // TODO: set on success and on error + this->makeRequest("games", urlQuery) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + auto data = root.value("data"); + + if (!data.isArray()) + { + failureCallback(); + return Failure; + } + + std::vector games; + + for (const auto &jsonStream : data.toArray()) + { + games.emplace_back(jsonStream.toObject()); + } + + successCallback(games); + + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Helix::getGameById(QString gameId, + ResultCallback successCallback, + HelixFailureCallback failureCallback) +{ + QStringList gameIds{gameId}; + QStringList gameNames; + + this->fetchGames( + gameIds, gameNames, + [successCallback, failureCallback](const auto &games) { + if (games.empty()) + { + failureCallback(); + return; + } + successCallback(games[0]); + }, + failureCallback); +} + +NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) +{ + assert(!url.startsWith("/")); + + if (this->clientId.isEmpty()) + { + qDebug() + << "Helix::makeRequest called without a client ID set BabyRage"; + // return boost::none; + } + + if (this->oauthToken.isEmpty()) + { + qDebug() + << "Helix::makeRequest called without an oauth token set BabyRage"; + // return boost::none; + } + + const QString baseUrl("https://api.twitch.tv/helix/"); + + QUrl fullUrl(baseUrl + url); + + fullUrl.setQuery(urlQuery); + + return NetworkRequest(fullUrl) + .timeout(5 * 1000) + .header("Accept", "application/json") + .header("Client-ID", this->clientId) + .header("Authorization", "Bearer " + this->oauthToken); +} + +void Helix::update(QString clientId, QString oauthToken) +{ + this->clientId = clientId; + this->oauthToken = oauthToken; +} + +void Helix::initialize() +{ + assert(instance == nullptr); + + instance = new Helix(); +} + +Helix *getHelix() +{ + assert(instance != nullptr); + + return instance; +} + +} // namespace chatterino diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp new file mode 100644 index 00000000..bfe54b69 --- /dev/null +++ b/src/providers/twitch/api/Helix.hpp @@ -0,0 +1,198 @@ +#pragma once + +#include "common/NetworkRequest.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace chatterino { + +using HelixFailureCallback = std::function; +template +using ResultCallback = std::function; + +struct HelixUser { + QString id; + QString login; + QString displayName; + QString description; + QString profileImageUrl; + int viewCount; + + explicit HelixUser(QJsonObject jsonObject) + : id(jsonObject.value("id").toString()) + , login(jsonObject.value("login").toString()) + , displayName(jsonObject.value("display_name").toString()) + , description(jsonObject.value("description").toString()) + , profileImageUrl(jsonObject.value("profile_image_url").toString()) + , viewCount(jsonObject.value("view_count").toInt()) + { + } +}; + +struct HelixUsersFollowsRecord { + QString fromId; + QString fromName; + QString toId; + QString toName; + QString followedAt; // date time object + + HelixUsersFollowsRecord() + : fromId("") + , fromName("") + , toId("") + , toName("") + , followedAt("") + { + } + + explicit HelixUsersFollowsRecord(QJsonObject jsonObject) + : fromId(jsonObject.value("from_id").toString()) + , fromName(jsonObject.value("from_name").toString()) + , toId(jsonObject.value("to_id").toString()) + , toName(jsonObject.value("to_name").toString()) + , followedAt(jsonObject.value("followed_at").toString()) + { + } +}; + +struct HelixUsersFollowsResponse { + int total; + std::vector data; + explicit HelixUsersFollowsResponse(QJsonObject jsonObject) + : total(jsonObject.value("total").toInt()) + { + const auto &jsonData = jsonObject.value("data").toArray(); + std::transform(jsonData.begin(), jsonData.end(), + std::back_inserter(this->data), + [](const QJsonValue &record) { + return HelixUsersFollowsRecord(record.toObject()); + }); + } +}; + +struct HelixStream { + QString id; // stream id + QString userId; + QString userName; + QString gameId; + QString type; + QString title; + int viewerCount; + QString startedAt; + QString language; + QString thumbnailUrl; + + HelixStream() + : id("") + , userId("") + , userName("") + , gameId("") + , type("") + , title("") + , viewerCount() + , startedAt("") + , language("") + , thumbnailUrl("") + { + } + + explicit HelixStream(QJsonObject jsonObject) + : id(jsonObject.value("id").toString()) + , userId(jsonObject.value("user_id").toString()) + , userName(jsonObject.value("user_name").toString()) + , gameId(jsonObject.value("game_id").toString()) + , type(jsonObject.value("type").toString()) + , title(jsonObject.value("title").toString()) + , viewerCount(jsonObject.value("viewer_count").toInt()) + , startedAt(jsonObject.value("started_at").toString()) + , language(jsonObject.value("language").toString()) + , thumbnailUrl(jsonObject.value("thumbnail_url").toString()) + { + } +}; + +struct HelixGame { + QString id; // stream id + QString name; + QString boxArtUrl; + + explicit HelixGame(QJsonObject jsonObject) + : id(jsonObject.value("id").toString()) + , name(jsonObject.value("name").toString()) + , boxArtUrl(jsonObject.value("box_art_url").toString()) + { + } +}; + +class Helix final : boost::noncopyable +{ +public: + // https://dev.twitch.tv/docs/api/reference#get-users + void fetchUsers(QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + void getUserByName(QString userName, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + void getUserById(QString userId, ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-users-follows + void fetchUsersFollows( + QString fromId, QString toId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getUserFollowers( + QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getUserFollow( + QString userId, QString targetId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-streams + void fetchStreams(QStringList userIds, QStringList userLogins, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + + void getStreamById(QString userId, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void getStreamByName(QString userName, + ResultCallback successCallback, + HelixFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/api/reference#get-games + void fetchGames(QStringList gameIds, QStringList gameNames, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + + void getGameById(QString gameId, ResultCallback successCallback, + HelixFailureCallback failureCallback); + + void update(QString clientId, QString oauthToken); + + static void initialize(); + +private: + NetworkRequest makeRequest(QString url, QUrlQuery urlQuery); + + QString clientId; + QString oauthToken; +}; + +Helix *getHelix(); + +} // namespace chatterino diff --git a/src/providers/twitch/api/Kraken.cpp b/src/providers/twitch/api/Kraken.cpp new file mode 100644 index 00000000..be7e98b3 --- /dev/null +++ b/src/providers/twitch/api/Kraken.cpp @@ -0,0 +1,104 @@ +#include "providers/twitch/api/Kraken.hpp" + +#include "common/Outcome.hpp" +#include "providers/twitch/TwitchCommon.hpp" + +namespace chatterino { + +static Kraken *instance = nullptr; + +void Kraken::getChannel(QString userId, + ResultCallback successCallback, + KrakenFailureCallback failureCallback) +{ + assert(!userId.isEmpty()); + + this->makeRequest("channels/" + userId, {}) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + + successCallback(root); + + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +void Kraken::getUser(QString userId, ResultCallback successCallback, + KrakenFailureCallback failureCallback) +{ + assert(!userId.isEmpty()); + + this->makeRequest("users/" + userId, {}) + .onSuccess([successCallback, failureCallback](auto result) -> Outcome { + auto root = result.parseJson(); + + successCallback(root); + + return Success; + }) + .onError([failureCallback](auto result) { + // TODO: make better xd + failureCallback(); + }) + .execute(); +} + +NetworkRequest Kraken::makeRequest(QString url, QUrlQuery urlQuery) +{ + assert(!url.startsWith("/")); + + if (this->clientId.isEmpty()) + { + qDebug() + << "Kraken::makeRequest called without a client ID set BabyRage"; + } + + const QString baseUrl("https://api.twitch.tv/kraken/"); + + QUrl fullUrl(baseUrl + url); + + fullUrl.setQuery(urlQuery); + + if (!this->oauthToken.isEmpty()) + { + return NetworkRequest(fullUrl) + .timeout(5 * 1000) + .header("Accept", "application/vnd.twitchtv.v5+json") + .header("Client-ID", this->clientId) + .header("Authorization", "OAuth " + this->oauthToken); + } + + return NetworkRequest(fullUrl) + .timeout(5 * 1000) + .header("Accept", "application/vnd.twitchtv.v5+json") + .header("Client-ID", this->clientId); +} + +void Kraken::update(QString clientId, QString oauthToken) +{ + this->clientId = clientId; + this->oauthToken = oauthToken; +} + +void Kraken::initialize() +{ + assert(instance == nullptr); + + instance = new Kraken(); + + getKraken()->update(getDefaultClientID(), ""); +} + +Kraken *getKraken() +{ + assert(instance != nullptr); + + return instance; +} + +} // namespace chatterino diff --git a/src/providers/twitch/api/Kraken.hpp b/src/providers/twitch/api/Kraken.hpp new file mode 100644 index 00000000..047173d6 --- /dev/null +++ b/src/providers/twitch/api/Kraken.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "common/NetworkRequest.hpp" + +#include +#include +#include +#include + +#include + +namespace chatterino { + +using KrakenFailureCallback = std::function; +template +using ResultCallback = std::function; + +struct KrakenChannel { + const QString status; + + KrakenChannel(QJsonObject jsonObject) + : status(jsonObject.value("status").toString()) + { + } +}; + +struct KrakenUser { + const QString createdAt; + + KrakenUser(QJsonObject jsonObject) + : createdAt(jsonObject.value("created_at").toString()) + { + } +}; + +class Kraken final : boost::noncopyable +{ +public: + // https://dev.twitch.tv/docs/v5/reference/users#follow-channel + void getChannel(QString userId, + ResultCallback resultCallback, + KrakenFailureCallback failureCallback); + + // https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id + void getUser(QString userId, ResultCallback resultCallback, + KrakenFailureCallback failureCallback); + + void update(QString clientId, QString oauthToken); + + static void initialize(); + +private: + NetworkRequest makeRequest(QString url, QUrlQuery urlQuery); + + QString clientId; + QString oauthToken; +}; + +Kraken *getKraken(); + +} // namespace chatterino diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md new file mode 100644 index 00000000..a95ac6bc --- /dev/null +++ b/src/providers/twitch/api/README.md @@ -0,0 +1,125 @@ +# Twitch API +this folder describes what sort of API requests we do, what permissions are required for the requests etc + +## Kraken (V5) +We use a bunch of Kraken (V5) in Chatterino2. + +### Get User +URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id + +Migration path: **Unknown** + + * We implement this in `providers/twitch/api/Kraken.cpp getUser` + Used in: + * `UserInfoPopup` to get the "created at" date of a user + +### Get Channel +URL: https://dev.twitch.tv/docs/v5/reference/channels#get-channel + +Migration path: **Unknown** + + * We implement this in `providers/twitch/api/Kraken.cpp getChannel` + Used in: + * `TwitchChannel::refreshTitle` to check the current stream title/game of offline channels + +### Follow Channel +URL: https://dev.twitch.tv/docs/v5/reference/users#follow-channel +Requires `user_follows_edit` scope + +Migration path: **Unknown** + + * We implement this API in `providers/twitch/TwitchAccount.cpp followUser` + +### Unfollow Channel +URL: https://dev.twitch.tv/docs/v5/reference/users#unfollow-channel +Requires `user_follows_edit` scope + +Migration path: **Unknown** + + * We implement this API in `providers/twitch/TwitchAccount.cpp unfollowUser` + + +### Get Cheermotes +URL: https://dev.twitch.tv/docs/v5/reference/bits#get-cheermotes + +Migration path: **Not checked** + + * We implement this API in `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` + +### Get User Block List +URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-block-list + +Migration path: **Unknown** + + * We use this in `providers/twitch/TwitchAccount.cpp loadIgnores` + +### Block User +URL: https://dev.twitch.tv/docs/v5/reference/users#block-user +Requires `user_blocks_edit` scope + +Migration path: **Unknown** + + * We use this in `providers/twitch/TwitchAccount.cpp ignoreByID` + +### Unblock User +URL: https://dev.twitch.tv/docs/v5/reference/users#unblock-user +Requires `user_blocks_edit` scope + +Migration path: **Unknown** + + * We use this in `providers/twitch/TwitchAccount.cpp unignoreByID` + +### Get User Emotes +URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes +Requires `user_subscriptions` scope + +Migration path: **Unknown** + + * We use this in `providers/twitch/TwitchAccount.cpp loadEmotes` to figure out which emotes a user is allowed to use! + +### AUTOMOD APPROVE +**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2 + + * We use this in `providers/twitch/TwitchAccount.cpp autoModAllow` to approve an automod deny/allow question + +### AUTOMOD DENY +**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2 + + * We use this in `providers/twitch/TwitchAccount.cpp autoModDeny` to deny an automod deny/allow question + +## Helix +Full Helix API reference: https://dev.twitch.tv/docs/api/reference + +### Get Users +URL: https://dev.twitch.tv/docs/api/reference#get-users + + * We implement this in `providers/twitch/api/Helix.cpp fetchUsers`. + Used in: + * `UserInfoPopup` to get ID and viewcount of username we clicked + * `CommandController` to power any commands that need to get a user ID + * `Toasts` to get the profile picture of a streamer who just went live + * `TwitchAccount` ignore and unignore features to translate user name to user ID + +### Get Users Follows +URL: https://dev.twitch.tv/docs/api/reference#get-users-follows + + * We implement this in `providers/twitch/api/Helix.cpp fetchUsersFollows` + Used in: + * `UserInfoPopup` to get number of followers a user has + +### Get Streams +URL: https://dev.twitch.tv/docs/api/reference#get-streams + + * We implement this in `providers/twitch/api/Helix.cpp fetchStreams` + Used in: + * `TwitchChannel` to get live status, game, title, and viewer count of a channel + * `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for + +## TMI +The TMI api is undocumented. + +### Get Chatters +**Undocumented** + + * We use this in `widgets/splits/Split.cpp showViewerList` + * We use this in `providers/twitch/TwitchChannel.cpp refreshChatters` diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index fc475d75..be7fdc48 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -2,8 +2,6 @@ #include "common/Singleton.hpp" -#define GIF_FRAME_LENGTH 33 - #include "providers/bttv/BttvEmotes.hpp" #include "providers/emoji/Emojis.hpp" #include "providers/ffz/FfzEmotes.hpp" diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 4d22f6fa..51d1cedc 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -187,8 +187,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage( return; } - qDebug() << root; - if (action == "select") { QString _type = root.value("type").toString(); diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 30ba15ec..60e38e55 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -1,19 +1,120 @@ #include "singletons/Settings.hpp" #include "Application.hpp" +#include "controllers/highlights/HighlightBlacklistUser.hpp" +#include "controllers/highlights/HighlightPhrase.hpp" +#include "controllers/ignores/IgnorePhrase.hpp" #include "singletons/Paths.hpp" #include "singletons/Resources.hpp" #include "singletons/WindowManager.hpp" +#include "util/PersistSignalVector.hpp" #include "util/WindowsHelper.hpp" namespace chatterino { +ConcurrentSettings *concurrentInstance_{}; + +ConcurrentSettings::ConcurrentSettings() + // NOTE: these do not get deleted + : highlightedMessages(*new SignalVector()) + , highlightedUsers(*new SignalVector()) + , blacklistedUsers(*new SignalVector()) + , ignoredMessages(*new SignalVector()) + , mutedChannels(*new SignalVector()) + , moderationActions(*new SignalVector) +{ + persist(this->highlightedMessages, "/highlighting/highlights"); + persist(this->blacklistedUsers, "/highlighting/blacklist"); + persist(this->highlightedUsers, "/highlighting/users"); + persist(this->ignoredMessages, "/ignore/phrases"); + persist(this->mutedChannels, "/pings/muted"); + // tagged users? + persist(this->moderationActions, "/moderation/actions"); +} + +bool ConcurrentSettings::isHighlightedUser(const QString &username) +{ + for (const auto &highlightedUser : this->highlightedUsers) + { + if (highlightedUser.isMatch(username)) + return true; + } + + return false; +} + +bool ConcurrentSettings::isBlacklistedUser(const QString &username) +{ + auto items = this->blacklistedUsers.readOnly(); + + for (const auto &blacklistedUser : *items) + { + if (blacklistedUser.isMatch(username)) + return true; + } + + return false; +} + +bool ConcurrentSettings::isMutedChannel(const QString &channelName) +{ + for (const auto &channel : this->mutedChannels) + { + if (channelName.toLower() == channel.toLower()) + { + return true; + } + } + return false; +} + +void ConcurrentSettings::mute(const QString &channelName) +{ + mutedChannels.append(channelName); +} + +void ConcurrentSettings::unmute(const QString &channelName) +{ + for (std::vector::size_type i = 0; i != mutedChannels.raw().size(); + i++) + { + if (mutedChannels.raw()[i].toLower() == channelName.toLower()) + { + mutedChannels.removeAt(i); + i--; + } + } +} + +bool ConcurrentSettings::toggleMutedChannel(const QString &channelName) +{ + if (this->isMutedChannel(channelName)) + { + unmute(channelName); + return false; + } + else + { + mute(channelName); + return true; + } +} + +ConcurrentSettings &getCSettings() +{ + // `concurrentInstance_` gets assigned in Settings ctor. + assert(concurrentInstance_); + + return *concurrentInstance_; +} + Settings *Settings::instance_ = nullptr; Settings::Settings(const QString &settingsDirectory) : ABSettings(settingsDirectory) { instance_ = this; + concurrentInstance_ = this; #ifdef USEWINSDK this->autorun = isRegisteredForStartup(); diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index f0d1679d..46f95075 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -5,13 +5,47 @@ #include "BaseSettings.hpp" #include "common/Channel.hpp" +#include "common/SignalVector.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" #include "singletons/Toasts.hpp" namespace chatterino { -class Settings : public ABSettings +class HighlightPhrase; +class HighlightBlacklistUser; +class IgnorePhrase; +class TaggedUser; + +/// Settings which are availlable for reading on all threads. +class ConcurrentSettings +{ +public: + ConcurrentSettings(); + + SignalVector &highlightedMessages; + SignalVector &highlightedUsers; + SignalVector &blacklistedUsers; + SignalVector &ignoredMessages; + SignalVector &mutedChannels; + //SignalVector &taggedUsers; + SignalVector &moderationActions; + + bool isHighlightedUser(const QString &username); + bool isBlacklistedUser(const QString &username); + bool isMutedChannel(const QString &channelName); + bool toggleMutedChannel(const QString &channelName); + +private: + void mute(const QString &channelName); + void unmute(const QString &channelName); +}; + +ConcurrentSettings &getCSettings(); + +/// Settings which are availlable for reading and writing on the gui thread. +// These settings are still accessed concurrently in the code but it is bad practice. +class Settings : public ABSettings, public ConcurrentSettings { static Settings *instance_; @@ -100,6 +134,8 @@ public: "/behaviour/autocompletion/smallStreamerLimit", 1000}; BoolSetting prefixOnlyEmoteCompletion = { "/behaviour/autocompletion/prefixOnlyCompletion", true}; + BoolSetting userCompletionOnlyWithAt = { + "/behaviour/autocompletion/userCompletionOnlyWithAt", false}; FloatSetting pauseOnHoverDuration = {"/behaviour/pauseOnHoverDuration", 0}; EnumSetting pauseChatModifier = { @@ -169,6 +205,17 @@ public: QStringSetting whisperHighlightColor = { "/highlighting/whisperHighlightColor", ""}; + BoolSetting enableRedeemedHighlight = { + "/highlighting/redeemedHighlight/highlighted", true}; + BoolSetting enableRedeemedHighlightSound = { + "/highlighting/redeemedHighlight/enableSound", false}; + BoolSetting enableRedeemedHighlightTaskbar = { + "/highlighting/redeemedHighlight/enableTaskbarFlashing", false}; + QStringSetting redeemedHighlightSoundUrl = { + "/highlighting/redeemedHighlightSoundUrl", ""}; + QStringSetting redeemedHighlightColor = { + "/highlighting/redeemedHighlightColor", ""}; + BoolSetting enableSubHighlight = { "/highlighting/subHighlight/subsHighlighted", true}; BoolSetting enableSubHighlightSound = { @@ -221,6 +268,9 @@ public: "Choose"}; QStringSetting streamlinkOpts = {"/external/streamlink/options", ""}; + // Custom URI Scheme + QStringSetting customURIScheme = {"/external/urischeme"}; + /// Misc BoolSetting betaUpdates = {"/misc/beta", false}; #ifdef Q_OS_LINUX @@ -239,6 +289,8 @@ public: BoolSetting restartOnCrash = {"/misc/restartOnCrash", false}; BoolSetting attachExtensionToAnyProcess = { "/misc/attachExtensionToAnyProcess", false}; + BoolSetting hideViewerCountAndDuration = { + "/misc/hideViewerCountAndDuration", false}; /// Debug BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages", diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 1876126e..0411e860 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -48,8 +48,6 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20); } - this->messages.backgrounds.highlighted = QColor(140, 84, 89, 127); - this->splits.header.background = getColor(0, sat, flat ? 1 : 0.9); this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85); this->splits.header.text = this->messages.textColors.regular; @@ -74,15 +72,6 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.background = getColor(0, sat, 1); this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); - - // Highlighted Messages - // hidden setting from PR #744 - if set it will overwrite theme color - // TODO: implement full theme support - if (getSettings()->highlightColor != "") - { - this->messages.backgrounds.highlighted = - QColor(getSettings()->highlightColor.getValue()); - } } void Theme::normalizeColor(QColor &color) diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index def5df00..9316a879 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -7,6 +7,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "providers/twitch/api/Helix.hpp" #include "singletons/Paths.hpp" #include "util/StreamLink.hpp" #include "widgets/helper/CommonTexts.hpp" @@ -85,14 +86,17 @@ void Toasts::sendChannelNotification(const QString &channelName, Platform p) } else { - this->fetchChannelAvatar( + getHelix()->getUserByName( channelName, - [channelName, sendChannelNotification](QString avatarLink) { + [channelName, sendChannelNotification](const auto &user) { DownloadManager *manager = new DownloadManager(); - manager->setFile(avatarLink, channelName); + manager->setFile(user.profileImageUrl, channelName); manager->connect(manager, &DownloadManager::downloadComplete, sendChannelNotification); + }, + [] { + // on failure }); } } @@ -206,53 +210,4 @@ void Toasts::sendWindowsNotification(const QString &channelName, Platform p) #endif -void Toasts::fetchChannelAvatar(const QString channelName, - std::function successCallback) -{ - QString requestUrl("https://api.twitch.tv/kraken/users?login=" + - channelName); - - NetworkRequest(requestUrl) - - .authorizeTwitchV5(getDefaultClientID()) - .timeout(30000) - .onSuccess([successCallback](auto result) mutable -> Outcome { - auto root = result.parseJson(); - if (!root.value("users").isArray()) - { - // log("API Error while getting user id, users is not an array"); - successCallback(""); - return Failure; - } - auto users = root.value("users").toArray(); - if (users.size() != 1) - { - // log("API Error while getting user id, users array size is not - // 1"); - successCallback(""); - return Failure; - } - if (!users[0].isObject()) - { - // log("API Error while getting user id, first user is not an - // object"); - successCallback(""); - return Failure; - } - auto firstUser = users[0].toObject(); - auto avatar = firstUser.value("logo"); - if (!avatar.isString()) - { - // log("API Error: while getting user avatar, first user object " - // "`avatar` key " - // "is not a " - // "string"); - successCallback(""); - return Failure; - } - successCallback(avatar.toString()); - return Success; - }) - .execute(); -} } // namespace chatterino diff --git a/src/singletons/Toasts.hpp b/src/singletons/Toasts.hpp index 43c6d1da..5ef792c7 100644 --- a/src/singletons/Toasts.hpp +++ b/src/singletons/Toasts.hpp @@ -29,9 +29,5 @@ private: #ifdef Q_OS_WIN void sendWindowsNotification(const QString &channelName, Platform p); #endif - - static void fetchChannelAvatar( - const QString channelName, - std::function successCallback); }; } // namespace chatterino diff --git a/src/singletons/TooltipPreviewImage.cpp b/src/singletons/TooltipPreviewImage.cpp index 5355b7d8..ebb0fe6a 100644 --- a/src/singletons/TooltipPreviewImage.cpp +++ b/src/singletons/TooltipPreviewImage.cpp @@ -5,6 +5,7 @@ #include "widgets/TooltipWidget.hpp" namespace chatterino { + TooltipPreviewImage &TooltipPreviewImage::instance() { static TooltipPreviewImage *instance = new TooltipPreviewImage(); @@ -13,19 +14,19 @@ TooltipPreviewImage &TooltipPreviewImage::instance() TooltipPreviewImage::TooltipPreviewImage() { - connections_.push_back(getApp()->windows->gifRepaintRequested.connect([&] { - auto tooltipWidget = TooltipWidget::instance(); - if (this->image_ && !tooltipWidget->isHidden()) + auto windows = getApp()->windows; + + this->connections_.push_back(windows->gifRepaintRequested.connect([&] { + if (this->image_ && this->image_->animated()) { - auto pixmap = this->image_->pixmapOrLoad(); - if (pixmap) - { - tooltipWidget->setImage(*pixmap); - } + this->refreshTooltipWidgetPixmap(); } - else + })); + + this->connections_.push_back(windows->miscUpdate.connect([&] { + if (this->attemptRefresh) { - tooltipWidget->clearImage(); + this->refreshTooltipWidgetPixmap(); } })); } @@ -33,5 +34,30 @@ TooltipPreviewImage::TooltipPreviewImage() void TooltipPreviewImage::setImage(ImagePtr image) { this->image_ = image; + + this->refreshTooltipWidgetPixmap(); } + +void TooltipPreviewImage::refreshTooltipWidgetPixmap() +{ + auto tooltipWidget = TooltipWidget::instance(); + + if (this->image_ && !tooltipWidget->isHidden()) + { + if (auto pixmap = this->image_->pixmapOrLoad()) + { + tooltipWidget->setImage(*pixmap); + this->attemptRefresh = false; + } + else + { + this->attemptRefresh = true; + } + } + else + { + tooltipWidget->clearImage(); + } +} + } // namespace chatterino diff --git a/src/singletons/TooltipPreviewImage.hpp b/src/singletons/TooltipPreviewImage.hpp index 5b23945f..0d1f6f07 100644 --- a/src/singletons/TooltipPreviewImage.hpp +++ b/src/singletons/TooltipPreviewImage.hpp @@ -3,6 +3,7 @@ #include "messages/Image.hpp" namespace chatterino { + class TooltipPreviewImage { public: @@ -17,5 +18,13 @@ private: private: ImagePtr image_ = nullptr; std::vector connections_; + + // attemptRefresh is set to true in case we want to preview an image that has not loaded yet (if pixmapOrLoad fails) + bool attemptRefresh{false}; + + // Refresh the pixmap used in the Tooltip Widget + // Called from setImage and from the "gif repaint" signal if the image is animated + void refreshTooltipWidgetPixmap(); }; + } // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index bddb948f..5c9f23ca 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -111,6 +111,12 @@ WindowManager::WindowManager() QObject::connect(this->saveTimer, &QTimer::timeout, [] { getApp()->windows->save(); // }); + + this->miscUpdateTimer_.start(100); + + QObject::connect(&this->miscUpdateTimer_, &QTimer::timeout, [this] { + this->miscUpdate.invoke(); // + }); } MessageElementFlags WindowManager::getWordFlags() @@ -268,6 +274,16 @@ Window *WindowManager::windowAt(int index) return this->windows_.at(index); } +QPoint WindowManager::emotePopupPos() +{ + return this->emotePopupPos_; +} + +void WindowManager::setEmotePopupPos(QPoint pos) +{ + this->emotePopupPos_ = pos; +} + void WindowManager::initialize(Settings &settings, Paths &paths) { assertInGuiThread(); @@ -395,6 +411,10 @@ void WindowManager::initialize(Settings &settings, Paths &paths) } window.show(); + QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject(); + this->emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(), + emote_popup_obj.value("y").toInt()); + if (window_obj.value("state") == "minimized") { window.setWindowState(Qt::WindowMinimized); @@ -472,6 +492,11 @@ void WindowManager::save() window_obj.insert("width", rect.width()); window_obj.insert("height", rect.height()); + QJsonObject emote_popup_obj; + emote_popup_obj.insert("x", this->emotePopupPos_.x()); + emote_popup_obj.insert("y", this->emotePopupPos_.y()); + window_obj.insert("emotePopup", emote_popup_obj); + // window tabs QJsonArray tabs_arr; diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 77084eaa..7ffe52e1 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -51,6 +51,9 @@ public: int windowCount(); Window *windowAt(int index); + QPoint emotePopupPos(); + void setEmotePopupPos(QPoint pos); + virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; void closeAll(); @@ -80,11 +83,17 @@ public: pajlada::Signals::NoArgSignal wordFlagsChanged; + // This signal fires every 100ms and can be used to trigger random things that require a recheck. + // It is currently being used by the "Tooltip Preview Image" system to recheck if an image is ready to be rendered. + pajlada::Signals::NoArgSignal miscUpdate; + private: void encodeNodeRecusively(SplitContainer::Node *node, QJsonObject &obj); bool initialized_ = false; + QPoint emotePopupPos_; + std::atomic generation_{0}; std::vector windows_; @@ -96,6 +105,7 @@ private: pajlada::SettingListener wordFlagsListener_; QTimer *saveTimer; + QTimer miscUpdateTimer_; }; } // namespace chatterino diff --git a/src/singletons/helper/GifTimer.cpp b/src/singletons/helper/GifTimer.cpp index 28e1b77e..2edf7bac 100644 --- a/src/singletons/helper/GifTimer.cpp +++ b/src/singletons/helper/GifTimer.cpp @@ -22,6 +22,7 @@ void GIFTimer::initialize() qApp->activeWindow() == nullptr) return; + this->position_ += gifFrameLength; this->signal.invoke(); getApp()->windows->repaintGifEmotes(); }); diff --git a/src/singletons/helper/GifTimer.hpp b/src/singletons/helper/GifTimer.hpp index b44fcfb7..da47149b 100644 --- a/src/singletons/helper/GifTimer.hpp +++ b/src/singletons/helper/GifTimer.hpp @@ -5,15 +5,22 @@ namespace chatterino { +constexpr long unsigned gifFrameLength = 33; + class GIFTimer { public: void initialize(); pajlada::Signals::NoArgSignal signal; + long unsigned position() + { + return this->position_; + } private: QTimer timer; + long unsigned position_{}; }; } // namespace chatterino diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 216c6f8c..460f8fcf 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -61,8 +61,6 @@ namespace { if (command.isNull()) return QString(); - qDebug() << command; - // inject switch to enable private browsing command = injectPrivateSwitch(command); if (command.isNull()) diff --git a/src/util/PersistSignalVector.hpp b/src/util/PersistSignalVector.hpp new file mode 100644 index 00000000..b1dde778 --- /dev/null +++ b/src/util/PersistSignalVector.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include "common/ChatterinoSetting.hpp" +#include "common/SignalVector.hpp" + +namespace chatterino { + +template +inline void persist(SignalVector &vec, const std::string &name) +{ + auto setting = std::make_unique>>(name); + + for (auto &&item : setting->getValue()) + vec.append(item); + + vec.delayedItemsChanged.connect([setting = setting.get(), vec = &vec] { + setting->setValue(vec->raw()); + }); + + // TODO: Delete when appropriate. + setting.release(); +} + +} // namespace chatterino diff --git a/src/util/StandardItemHelper.hpp b/src/util/StandardItemHelper.hpp index 4b7ba52b..f9caeb0a 100644 --- a/src/util/StandardItemHelper.hpp +++ b/src/util/StandardItemHelper.hpp @@ -4,12 +4,20 @@ namespace chatterino { +static auto defaultItemFlags(bool selectable) +{ + return Qt::ItemIsEnabled | + (selectable ? Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | + Qt::ItemIsDropEnabled + : Qt::ItemFlag()); +} + static void setBoolItem(QStandardItem *item, bool value, bool userCheckable = true, bool selectable = true) { - item->setFlags(Qt::ItemFlags( - Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable : 0) | - (userCheckable ? Qt::ItemIsUserCheckable : 0))); + item->setFlags( + Qt::ItemFlags(defaultItemFlags(selectable) | + (userCheckable ? Qt::ItemIsUserCheckable : 0))); item->setCheckState(value ? Qt::Checked : Qt::Unchecked); } @@ -17,28 +25,33 @@ static void setStringItem(QStandardItem *item, const QString &value, bool editable = true, bool selectable = true) { item->setData(value, Qt::EditRole); - item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | - (selectable ? Qt::ItemIsSelectable : 0) | + item->setFlags(Qt::ItemFlags(defaultItemFlags(selectable) | (editable ? (Qt::ItemIsEditable) : 0))); } -static void setFilePathItem(QStandardItem *item, const QUrl &value) +static void setFilePathItem(QStandardItem *item, const QUrl &value, + bool selectable = true) { item->setData(value, Qt::UserRole); item->setData(value.fileName(), Qt::DisplayRole); - item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable)); + item->setFlags( + Qt::ItemFlags(defaultItemFlags(selectable) | + (selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags))); } -static void setColorItem(QStandardItem *item, const QColor &value) +static void setColorItem(QStandardItem *item, const QColor &value, + bool selectable = true) { item->setData(value, Qt::DecorationRole); - item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable)); + item->setFlags( + Qt::ItemFlags(defaultItemFlags(selectable) | + (selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags))); } static QStandardItem *emptyItem() { auto *item = new QStandardItem(); - item->setFlags(Qt::ItemFlags(0)); + item->setFlags(Qt::ItemFlags()); return item; } diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 04c0dde0..83f7dbc0 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -29,8 +29,6 @@ BOOL CALLBACK enumWindows(HWND hwnd, LPARAM) auto className = std::make_unique(length); GetClassName(hwnd, className.get(), length); - // qDebug() << QString::fromWCharArray(className.get(), length); - if (lstrcmp(className.get(), L"Shell_TrayWnd") == 0 || lstrcmp(className.get(), L"Shell_Secondary") == 0) { diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index b1e939db..c3bc34c9 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -384,7 +384,6 @@ void BaseWindow::mousePressEvent(QMouseEvent *event) if (!recursiveCheckMouseTracking(widget)) { - qDebug() << "Start moving"; this->moving = true; } } @@ -401,7 +400,6 @@ void BaseWindow::mouseReleaseEvent(QMouseEvent *event) { if (this->moving) { - qDebug() << "Stop moving"; this->moving = false; } } diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index b0fa87e6..f840cea3 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -149,7 +149,6 @@ void Notebook::select(QWidget *page) { if (containsChild(page, item.selectedWidget)) { - qDebug() << item.selectedWidget; item.selectedWidget->setFocus(Qt::MouseFocusReason); } else diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 9af6c307..bdecc361 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -3,6 +3,7 @@ #include "Application.hpp" #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" +#include "singletons/WindowManager.hpp" #include "widgets/helper/ChannelView.hpp" #include @@ -242,6 +243,8 @@ void Scrollbar::paintEvent(QPaintEvent *) QPainter painter(this); painter.fillRect(rect(), this->theme->scrollbars.background); + bool enableRedeemedHighlights = getSettings()->enableRedeemedHighlight; + // painter.fillRect(QRect(xOffset, 0, width(), this->buttonHeight), // this->themeManager->ScrollbarArrow); // painter.fillRect(QRect(xOffset, height() - this->buttonHeight, @@ -274,7 +277,8 @@ void Scrollbar::paintEvent(QPaintEvent *) int w = this->width(); float y = 0; float dY = float(this->height()) / float(snapshotLength); - int highlightHeight = int(std::ceil(dY)); + int highlightHeight = + int(std::ceil(std::max(this->scale() * 2, dY))); for (size_t i = 0; i < snapshotLength; i++) { @@ -282,22 +286,26 @@ void Scrollbar::paintEvent(QPaintEvent *) if (!highlight.isNull()) { - QColor color = highlight.getColor(); - - switch (highlight.getStyle()) + if (!highlight.isRedeemedHighlight() || enableRedeemedHighlights) { - case ScrollbarHighlight::Default: { - painter.fillRect(w / 8 * 3, int(y), w / 4, highlightHeight, - color); - } - break; + QColor color = highlight.getColor(); + color.setAlpha(255); - case ScrollbarHighlight::Line: { - painter.fillRect(0, int(y), w, 1, color); - } - break; + switch (highlight.getStyle()) + { + case ScrollbarHighlight::Default: { + painter.fillRect(w / 8 * 3, int(y), w / 4, + highlightHeight, color); + } + break; - case ScrollbarHighlight::None:; + case ScrollbarHighlight::Line: { + painter.fillRect(0, int(y), w, 1, color); + } + break; + + case ScrollbarHighlight::None:; + } } } diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index c000e3ec..8126830c 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -1,12 +1,14 @@ #include "EmotePopup.hpp" #include "Application.hpp" +#include "common/CompletionModel.hpp" #include "controllers/accounts/AccountController.hpp" #include "debug/Benchmark.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Emotes.hpp" +#include "singletons/WindowManager.hpp" #include "util/Shortcut.hpp" #include "widgets/Notebook.hpp" #include "widgets/helper/ChannelView.hpp" @@ -37,7 +39,8 @@ namespace { std::sort(vec.begin(), vec.end(), [](const std::pair &l, const std::pair &r) { - return l.first.string < r.first.string; + return CompletionModel::compareStrings( + l.first.string, r.first.string); }); for (const auto &emote : vec) { @@ -111,6 +114,8 @@ namespace { EmotePopup::EmotePopup(QWidget *parent) : BasePopup(BaseWindow::EnableCustomFrame, parent) { + this->moveTo(this, getApp()->windows->emotePopupPos(), false); + auto layout = new QVBoxLayout(this); this->getLayoutContainer()->setLayout(layout); @@ -218,4 +223,9 @@ void EmotePopup::loadEmojis() this->viewEmojis_->setChannel(emojiChannel); } +void EmotePopup::closeEvent(QCloseEvent *event) +{ + getApp()->windows->setEmotePopupPos(this->pos()); + QWidget::closeEvent(event); +} } // namespace chatterino diff --git a/src/widgets/dialogs/EmotePopup.hpp b/src/widgets/dialogs/EmotePopup.hpp index 7e24faf9..18647f26 100644 --- a/src/widgets/dialogs/EmotePopup.hpp +++ b/src/widgets/dialogs/EmotePopup.hpp @@ -19,6 +19,8 @@ public: void loadChannel(ChannelPtr channel); void loadEmojis(); + virtual void closeEvent(QCloseEvent *event) override; + pajlada::Signals::Signal linkClicked; private: diff --git a/src/widgets/dialogs/LoginDialog.cpp b/src/widgets/dialogs/LoginDialog.cpp index f96c8634..3666ea65 100644 --- a/src/widgets/dialogs/LoginDialog.cpp +++ b/src/widgets/dialogs/LoginDialog.cpp @@ -4,7 +4,6 @@ #include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "controllers/accounts/AccountController.hpp" -#include "providers/twitch/PartialTwitchUser.hpp" #include "util/Helpers.hpp" #ifdef USEWINSDK @@ -159,7 +158,6 @@ AdvancedLoginWidget::AdvancedLoginWidget() this->ui_.layout.addWidget(&this->ui_.instructionsLabel); this->ui_.layout.addLayout(&this->ui_.formLayout); this->ui_.layout.addLayout(&this->ui_.buttonUpperRow.layout); - this->ui_.layout.addLayout(&this->ui_.buttonLowerRow.layout); this->refreshButtons(); @@ -207,29 +205,10 @@ AdvancedLoginWidget::AdvancedLoginWidget() LogInWithCredentials(userID, username, clientID, oauthToken); }); - - /// Lower button row - this->ui_.buttonLowerRow.fillInUserIDButton.setText( - "Get user ID from username"); - - this->ui_.buttonLowerRow.layout.addWidget( - &this->ui_.buttonLowerRow.fillInUserIDButton); - - connect(&this->ui_.buttonLowerRow.fillInUserIDButton, &QPushButton::clicked, - [=]() { - const auto onIdFetched = [=](const QString &userID) { - this->ui_.userIDInput.setText(userID); // - }; - PartialTwitchUser::byName(this->ui_.usernameInput.text()) - .getId(onIdFetched, this); - }); } void AdvancedLoginWidget::refreshButtons() { - this->ui_.buttonLowerRow.fillInUserIDButton.setEnabled( - !this->ui_.usernameInput.text().isEmpty()); - if (this->ui_.userIDInput.text().isEmpty() || this->ui_.usernameInput.text().isEmpty() || this->ui_.clientIDInput.text().isEmpty() || diff --git a/src/widgets/dialogs/LoginDialog.hpp b/src/widgets/dialogs/LoginDialog.hpp index a6819e3a..796b98ca 100644 --- a/src/widgets/dialogs/LoginDialog.hpp +++ b/src/widgets/dialogs/LoginDialog.hpp @@ -57,12 +57,6 @@ public: QPushButton addUserButton; QPushButton clearFieldsButton; } buttonUpperRow; - - struct { - QHBoxLayout layout; - - QPushButton fillInUserIDButton; - } buttonLowerRow; } ui_; }; diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index 18e1f1a0..0afd6b9b 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -4,7 +4,6 @@ #include "common/Channel.hpp" #include "common/NetworkRequest.hpp" #include "messages/Message.hpp" -#include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "util/PostToThread.hpp" diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index 25d61ff0..a17c7022 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -152,7 +152,7 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) auto editor = new IrcConnectionEditor(unique); if (editor->exec() == QDialog::Accepted) { - Irc::instance().connections.appendItem(editor->data()); + Irc::instance().connections.append(editor->data()); } }); @@ -160,21 +160,20 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) view->getTableView(), &QTableView::doubleClicked, [](const QModelIndex &index) { auto editor = new IrcConnectionEditor( - Irc::instance() - .connections.getVector()[size_t(index.row())]); + Irc::instance().connections.raw()[size_t(index.row())]); if (editor->exec() == QDialog::Accepted) { auto data = editor->data(); - auto &&conns = Irc::instance().connections.getVector(); + auto &&conns = Irc::instance().connections.raw(); int i = 0; for (auto &&conn : conns) { if (conn.id == data.id) { - Irc::instance().connections.removeItem( + Irc::instance().connections.removeAt( i, Irc::noEraseCredentialCaller); - Irc::instance().connections.insertItem(data, i); + Irc::instance().connections.insert(data, i); } i++; } @@ -348,7 +347,7 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const ->currentIndex() .row(); - auto &&vector = Irc::instance().connections.getVector(); + auto &&vector = Irc::instance().connections.raw(); if (row >= 0 && row < int(vector.size())) { diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index 73ad5f75..7effdb89 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -4,7 +4,6 @@ #include "singletons/Resources.hpp" #include "util/LayoutCreator.hpp" #include "widgets/helper/Button.hpp" -#include "widgets/helper/SettingsDialogTab.hpp" #include "widgets/settingspages/AboutPage.hpp" #include "widgets/settingspages/AccountsPage.hpp" #include "widgets/settingspages/CommandPage.hpp" @@ -21,22 +20,18 @@ namespace chatterino { -SettingsDialog *SettingsDialog::handle = nullptr; - SettingsDialog::SettingsDialog() : BaseWindow(BaseWindow::DisableCustomScaling) { this->setWindowTitle("Chatterino Settings"); + this->resize(815, 600); + this->themeChangedEvent(); + this->scaleChangedEvent(this->scale()); this->initUi(); this->addTabs(); - - this->scaleChangedEvent(this->scale()); - this->overrideBackgroundColor_ = QColor("#111111"); - this->themeChangedEvent(); - - this->resize(815, 600); + this->scaleChangedEvent(this->scale()); // execute twice to width of item } void SettingsDialog::initUi() @@ -69,13 +64,9 @@ void SettingsDialog::initUi() .assign(&this->ui_.tabContainer); // right side (pages) - auto right = - centerBox.emplace().withoutMargin().withoutSpacing(); - { - right.emplace() - .assign(&this->ui_.pageStack) - .withoutMargin(); - } + centerBox.emplace() + .assign(&this->ui_.pageStack) + .withoutMargin(); this->ui_.pageStack->setMargin(0); @@ -103,10 +94,11 @@ void SettingsDialog::initUi() void SettingsDialog::filterElements(const QString &text) { // filter elements and hide pages - for (auto &&page : this->pages_) + for (auto &&tab : this->tabs_) { // filterElements returns true if anything on the page matches the search query - page->tab()->setVisible(page->filterElements(text)); + tab->setVisible(tab->page()->filterElements(text) || + tab->name().contains(text, Qt::CaseInsensitive)); } // find next visible page @@ -144,11 +136,6 @@ void SettingsDialog::filterElements(const QString &text) } } -SettingsDialog *SettingsDialog::getHandle() -{ - return SettingsDialog::handle; -} - void SettingsDialog::addTabs() { this->ui_.tabContainer->setMargin(0); @@ -156,38 +143,34 @@ void SettingsDialog::addTabs() this->ui_.tabContainer->setContentsMargins(0, 20, 0, 20); - this->addTab(new GeneralPage); + // Constructors are wrapped in std::function to remove some strain from first time loading. + // clang-format off + this->addTab([]{return new GeneralPage;}, "General", ":/settings/about.svg"); this->ui_.tabContainer->addSpacing(16); - - this->addTab(new AccountsPage); - + this->addTab([]{return new AccountsPage;}, "Accounts", ":/settings/accounts.svg", SettingsTabId::Accounts); this->ui_.tabContainer->addSpacing(16); - - this->addTab(new CommandPage); - this->addTab(new HighlightingPage); - this->addTab(new IgnoresPage); - + this->addTab([]{return new CommandPage;}, "Commands", ":/settings/commands.svg"); + this->addTab([]{return new HighlightingPage;}, "Highlights", ":/settings/notifications.svg"); + this->addTab([]{return new IgnoresPage;}, "Ignores", ":/settings/ignore.svg"); this->ui_.tabContainer->addSpacing(16); - - this->addTab(new KeyboardSettingsPage); - this->addTab(this->ui_.moderationPage = new ModerationPage); - this->addTab(new NotificationPage); - this->addTab(new ExternalToolsPage); - + this->addTab([]{return new KeyboardSettingsPage;}, "Keybindings", ":/settings/keybinds.svg"); + this->addTab([]{return new ModerationPage;}, "Moderation", ":/settings/moderation.svg", SettingsTabId::Moderation); + this->addTab([]{return new NotificationPage;}, "Notifications", ":/settings/notification2.svg"); + this->addTab([]{return new ExternalToolsPage;}, "External tools", ":/settings/externaltools.svg"); this->ui_.tabContainer->addStretch(1); - this->addTab(new AboutPage, Qt::AlignBottom); + this->addTab([]{return new AboutPage;}, "About", ":/settings/about.svg", SettingsTabId(), Qt::AlignBottom); + // clang-format on } -void SettingsDialog::addTab(SettingsPage *page, Qt::Alignment alignment) +void SettingsDialog::addTab(std::function page, + const QString &name, const QString &iconPath, + SettingsTabId id, Qt::Alignment alignment) { - auto tab = new SettingsDialogTab(this, page, page->getIconResource()); - page->setTab(tab); + auto tab = new SettingsDialogTab(this, std::move(page), name, iconPath, id); - this->ui_.pageStack->addWidget(page); this->ui_.tabContainer->addWidget(tab, 0, alignment); this->tabs_.push_back(tab); - this->pages_.push_back(page); if (this->tabs_.size() == 1) { @@ -197,7 +180,16 @@ void SettingsDialog::addTab(SettingsPage *page, Qt::Alignment alignment) void SettingsDialog::selectTab(SettingsDialogTab *tab, bool byUser) { - this->ui_.pageStack->setCurrentWidget(tab->getSettingsPage()); + // add page if it's not been added yet + [&] { + for (int i = 0; i < this->ui_.pageStack->count(); i++) + if (this->ui_.pageStack->itemAt(i)->widget() == tab->page()) + return; + + this->ui_.pageStack->addWidget(tab->page()); + }(); + + this->ui_.pageStack->setCurrentWidget(tab->page()); if (this->selectedTab_ != nullptr) { @@ -215,28 +207,49 @@ void SettingsDialog::selectTab(SettingsDialogTab *tab, bool byUser) } } -void SettingsDialog::selectPage(SettingsPage *page) +void SettingsDialog::selectTab(SettingsTabId id) { - assert(page); - assert(page->tab()); + auto t = this->tab(id); + assert(t); + if (!t) + return; - this->selectTab(page->tab()); + this->selectTab(t); +} + +SettingsDialogTab *SettingsDialog::tab(SettingsTabId id) +{ + for (auto &&tab : this->tabs_) + if (tab->id() == id) + return tab; + + assert(false); + return nullptr; } void SettingsDialog::showDialog(SettingsDialogPreference preferredTab) { static SettingsDialog *instance = new SettingsDialog(); - instance->refresh(); + static bool hasShownBefore = false; + if (hasShownBefore) + instance->refresh(); + hasShownBefore = true; switch (preferredTab) { case SettingsDialogPreference::Accounts: - instance->selectTab(instance->tabs_.at(1)); + instance->selectTab(SettingsTabId::Accounts); break; case SettingsDialogPreference::ModerationActions: - instance->selectPage(instance->ui_.moderationPage); - instance->ui_.moderationPage->selectModerationActions(); + if (auto tab = instance->tab(SettingsTabId::Moderation)) + { + instance->selectTab(tab); + if (auto page = dynamic_cast(tab->page())) + { + page->selectModerationActions(); + } + } break; default:; @@ -250,11 +263,13 @@ void SettingsDialog::showDialog(SettingsDialogPreference preferredTab) void SettingsDialog::refresh() { + // Resets the cancel button. getSettings()->saveSnapshot(); + // Updates tabs. for (auto *tab : this->tabs_) { - tab->getSettingsPage()->onShow(); + tab->page()->onShow(); } } @@ -273,7 +288,8 @@ void SettingsDialog::scaleChangedEvent(float newDpi) this->setStyleSheet(styleSheet); - this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi)); + if (this->ui_.tabContainerContainer) + this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi)); } void SettingsDialog::themeChangedEvent() @@ -281,7 +297,7 @@ void SettingsDialog::themeChangedEvent() BaseWindow::themeChangedEvent(); QPalette palette; - palette.setColor(QPalette::Background, QColor("#111")); + palette.setColor(QPalette::Window, QColor("#111")); this->setPalette(palette); } @@ -301,7 +317,7 @@ void SettingsDialog::onCancelClicked() { for (auto &tab : this->tabs_) { - tab->getSettingsPage()->cancel(); + tab->page()->cancel(); } getSettings()->restoreSnapshot(); diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index 5a064b66..0d08f7c9 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include "widgets/helper/SettingsDialogTab.hpp" class QLineEdit; @@ -32,7 +34,6 @@ class SettingsDialog : public BaseWindow public: SettingsDialog(); - static SettingsDialog *getHandle(); // may be NULL static void showDialog(SettingsDialogPreference preferredTab = SettingsDialogPreference::NoPreference); @@ -42,15 +43,16 @@ protected: virtual void showEvent(QShowEvent *) override; private: - static SettingsDialog *handle; - void refresh(); void initUi(); + SettingsDialogTab *tab(SettingsTabId id); void addTabs(); - void addTab(SettingsPage *page, Qt::Alignment alignment = Qt::AlignTop); - void selectTab(SettingsDialogTab *tab, bool byUser = true); - void selectPage(SettingsPage *page); + void addTab(std::function page, const QString &name, + const QString &iconPath, SettingsTabId id = {}, + Qt::Alignment alignment = Qt::AlignTop); + void selectTab(SettingsDialogTab *tab, const bool byUser = true); + void selectTab(SettingsTabId id); void filterElements(const QString &query); void onOkClicked(); @@ -62,11 +64,9 @@ private: QStackedLayout *pageStack{}; QPushButton *okButton{}; QPushButton *cancelButton{}; - ModerationPage *moderationPage{}; QLineEdit *search{}; } ui_; std::vector tabs_; - std::vector pages_; SettingsDialogTab *selectedTab_{}; SettingsDialogTab *lastSelectedByUser_{}; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index e530a5bf..c399f8ef 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -4,10 +4,12 @@ #include "common/Channel.hpp" #include "common/NetworkRequest.hpp" #include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" -#include "providers/twitch/PartialTwitchUser.hpp" +#include "controllers/highlights/HighlightBlacklistUser.hpp" #include "providers/twitch/TwitchChannel.hpp" +#include "providers/twitch/api/Helix.hpp" +#include "providers/twitch/api/Kraken.hpp" #include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" #include "util/LayoutCreator.hpp" #include "util/PostToThread.hpp" #include "widgets/Label.hpp" @@ -21,9 +23,9 @@ #include #include -#define TEXT_FOLLOWERS "Followers: " -#define TEXT_VIEWS "Views: " -#define TEXT_CREATED "Created: " +const QString TEXT_VIEWS("Views: %1"); +const QString TEXT_FOLLOWERS("Followers: %1"); +const QString TEXT_CREATED("Created: %1"); #define TEXT_USER_ID "ID: " #define TEXT_UNAVAILABLE "(not available)" @@ -91,10 +93,11 @@ UserInfoPopup::UserInfoPopup() this->ui_.userIDLabel->setPalette(palette); } - vbox.emplace