diff --git a/.clang-format b/.clang-format index 6e663cb67..bd3f2c430 100644 --- a/.clang-format +++ b/.clang-format @@ -1,23 +1,14 @@ -IndentCaseLabels: true -BasedOnStyle: Google -IndentWidth: 4 -Standard: Auto -PointerBindsToType: false Language: Cpp -SpacesBeforeTrailingComments: 2 + AccessModifierOffset: -1 +AccessModifierOffset: -4 AlignEscapedNewlinesLeft: true +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: true +AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: false AlwaysBreakBeforeMultilineStrings: false -BreakConstructorInitializersBeforeComma: true -# BreakBeforeBraces: Linux -BreakBeforeBraces: Custom -AccessModifierOffset: -4 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -AllowShortFunctionsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLoopsOnASingleLine: false -DerivePointerBinding: false +BasedOnStyle: Google BraceWrapping: { AfterNamespace: 'false' AfterClass: 'true' @@ -26,5 +17,14 @@ BraceWrapping: { AfterFunction: 'true' BeforeCatch: 'false' } -ColumnLimit: 100 +BreakBeforeBraces: Custom +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 80 +ConstructorInitializerAllOnOneLineOrOnePerLine: false +DerivePointerBinding: false FixNamespaceComments: true +IndentCaseLabels: true +IndentWidth: 4 +PointerBindsToType: false +SpacesBeforeTrailingComments: 2 +Standard: Auto diff --git a/BUILDING_ON_MAC.md b/BUILDING_ON_MAC.md index 4c2421015..c09d97d24 100644 --- a/BUILDING_ON_MAC.md +++ b/BUILDING_ON_MAC.md @@ -3,7 +3,9 @@ 1. Install Xcode and Xcode Command Line Utilites 2. Start Xcode, settings -> Locations, activate your Command Line Tools 3. Install brew https://brew.sh/ -4. `brew install boost openssl rapidjson qt` +4. `brew install boost openssl rapidjson` +5. `brew install qt` +6. Step 5 should output some directions to add qt to your path, you will need to do this for qmake 5. Go into project directory 6. Create build folder `mkdir build && cd build` 7. `qmake .. && make` diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..621ef91eb --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,20 @@ +pipeline { + agent any + + stages { + stage('Build') { + parallel { + stage('GCC') { + steps { + sh 'mkdir -p build-linux-gcc && cd build-linux-gcc && make distclean; qmake .. && make' + } + } + stage('Clang') { + steps { + sh 'mkdir -p build-linux-clang && cd build-linux-clang && make distclean; qmake -spec linux-clang .. && make' + } + } + } + } + } +} diff --git a/README.md b/README.md index 0eba5a029..8ce67f521 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ Before building run `git submodule update --init --recursive` to get required su ## Code style The code is formated using clang format in Qt Creator. [.clang-format](https://github.com/fourtf/chatterino2/blob/master/.clang-format) contains the style file for clang format. -To setup automatic code formating with QT Creator, see [this guide](https://gist.github.com/pajlada/0296454198eb8f8789fd6fe7ea660c5b). - ### Get it automated with QT Creator + Beautifier + Clang Format 1. Download LLVM: http://releases.llvm.org/6.0.1/LLVM-6.0.1-win64.exe 2. During the installation, make sure to add it to your path diff --git a/chatterino.pro b/chatterino.pro index 22554da5f..7ddf1fd34 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -6,7 +6,7 @@ message(----) -QT += widgets core gui network multimedia svg +QT += widgets core gui network multimedia svg concurrent CONFIG += communi COMMUNI += core model util CONFIG += c++14 @@ -17,6 +17,10 @@ DEFINES += QT_DEPRECATED_WARNINGS PRECOMPILED_HEADER = src/PrecompiledHeader.hpp CONFIG += precompile_header +debug { + DEFINES += QT_DEBUG +} + useBreakpad { LIBS += -L$$PWD/lib/qBreakpad/handler/build include(lib/qBreakpad/qBreakpad.pri) @@ -101,7 +105,6 @@ SOURCES += \ src/Application.cpp \ src/common/Channel.cpp \ src/common/CompletionModel.cpp \ - src/common/Emotemap.cpp \ src/common/NetworkData.cpp \ src/common/NetworkManager.cpp \ src/common/NetworkRequest.cpp \ @@ -132,9 +135,7 @@ SOURCES += \ src/messages/MessageBuilder.cpp \ src/messages/MessageColor.cpp \ src/messages/MessageElement.cpp \ - src/providers/bttv/BttvEmotes.cpp \ src/providers/emoji/Emojis.cpp \ - src/providers/ffz/FfzEmotes.cpp \ src/providers/irc/AbstractIrcServer.cpp \ src/providers/irc/IrcAccount.cpp \ src/providers/irc/IrcChannel2.cpp \ @@ -184,8 +185,6 @@ SOURCES += \ src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/ResizingTextEdit.cpp \ - src/widgets/helper/RippleEffectButton.cpp \ - src/widgets/helper/RippleEffectLabel.cpp \ src/widgets/helper/ScrollbarHighlight.cpp \ src/widgets/helper/SearchPopup.cpp \ src/widgets/helper/SettingsDialogTab.cpp \ @@ -231,17 +230,35 @@ SOURCES += \ src/util/InitUpdateButton.cpp \ src/widgets/dialogs/UpdateDialog.cpp \ src/widgets/settingspages/IgnoresPage.cpp \ - src/providers/twitch/PubsubClient.cpp + src/providers/twitch/PubsubClient.cpp \ + src/providers/twitch/TwitchApi.cpp \ + src/messages/Emote.cpp \ + src/messages/ImageSet.cpp \ + src/providers/bttv/BttvEmotes.cpp \ + src/providers/ffz/FfzEmotes.cpp \ + src/autogenerated/ResourcesAutogen.cpp \ + src/singletons/Badges.cpp \ + src/providers/twitch/TwitchBadges.cpp \ + src/providers/chatterino/ChatterinoBadges.cpp \ + src/providers/twitch/TwitchParseCheerEmotes.cpp \ + src/providers/bttv/LoadBttvChannelEmote.cpp \ + src/util/JsonQuery.cpp \ + src/RunGui.cpp \ + src/BrowserExtension.cpp \ + src/util/FormatTime.cpp \ + src/util/FunctionEventFilter.cpp \ + src/widgets/helper/EffectLabel.cpp \ + src/widgets/helper/Button.cpp \ + src/messages/MessageContainer.cpp \ + src/debug/Benchmark.cpp HEADERS += \ src/Application.hpp \ src/common/Channel.hpp \ src/common/Common.hpp \ src/common/CompletionModel.hpp \ - src/common/Emotemap.hpp \ src/common/FlagsEnum.hpp \ - src/common/LockedObject.hpp \ - src/common/MutexValue.hpp \ + src/common/Atomic.hpp \ src/common/NetworkCommon.hpp \ src/common/NetworkData.hpp \ src/common/NetworkManager.hpp \ @@ -253,9 +270,8 @@ HEADERS += \ src/common/NullablePtr.hpp \ src/common/Property.hpp \ src/common/ProviderId.hpp \ - src/common/SerializeCustom.hpp \ + src/util/RapidJsonSerializeQString.hpp \ src/common/SignalVectorModel.hpp \ - src/common/UrlFetch.hpp \ src/common/Version.hpp \ src/controllers/accounts/Account.hpp \ src/controllers/accounts/AccountController.hpp \ @@ -289,12 +305,9 @@ HEADERS += \ src/messages/MessageBuilder.hpp \ src/messages/MessageColor.hpp \ src/messages/MessageElement.hpp \ - src/messages/MessageParseArgs.hpp \ src/messages/Selection.hpp \ src/PrecompiledHeader.hpp \ - src/providers/bttv/BttvEmotes.hpp \ src/providers/emoji/Emojis.hpp \ - src/providers/ffz/FfzEmotes.hpp \ src/providers/irc/AbstractIrcServer.hpp \ src/providers/irc/IrcAccount.hpp \ src/providers/irc/IrcChannel2.hpp \ @@ -358,8 +371,6 @@ HEADERS += \ src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookTab.hpp \ src/widgets/helper/ResizingTextEdit.hpp \ - src/widgets/helper/RippleEffectButton.hpp \ - src/widgets/helper/RippleEffectLabel.hpp \ src/widgets/helper/ScrollbarHighlight.hpp \ src/widgets/helper/SearchPopup.hpp \ src/widgets/helper/SettingsDialogTab.hpp \ @@ -402,7 +413,6 @@ HEADERS += \ src/singletons/Updates.hpp \ src/singletons/NativeMessaging.hpp \ src/singletons/Theme.hpp \ - src/common/SimpleSignalVector.hpp \ src/common/SignalVector.hpp \ src/widgets/dialogs/LogsPopup.hpp \ src/common/Singleton.hpp \ @@ -412,10 +422,34 @@ HEADERS += \ src/util/InitUpdateButton.hpp \ src/widgets/dialogs/UpdateDialog.hpp \ src/widgets/settingspages/IgnoresPage.hpp \ - src/providers/twitch/PubsubClient.hpp + src/providers/twitch/PubsubClient.hpp \ + src/providers/twitch/TwitchApi.hpp \ + src/messages/Emote.hpp \ + src/messages/EmoteCache.hpp \ + src/messages/ImageSet.hpp \ + src/common/Outcome.hpp \ + src/providers/bttv/BttvEmotes.hpp \ + src/providers/ffz/FfzEmotes.hpp \ + src/autogenerated/ResourcesAutogen.hpp \ + src/singletons/Badges.hpp \ + src/providers/twitch/TwitchBadges.hpp \ + src/providers/chatterino/ChatterinoBadges.hpp \ + src/common/Aliases.hpp \ + src/providers/twitch/TwitchParseCheerEmotes.hpp \ + src/providers/bttv/LoadBttvChannelEmote.hpp \ + src/util/JsonQuery.hpp \ + src/RunGui.hpp \ + src/BrowserExtension.hpp \ + src/util/FormatTime.hpp \ + src/util/FunctionEventFilter.hpp \ + src/widgets/helper/EffectLabel.hpp \ + src/util/LayoutHelper.hpp \ + src/widgets/helper/Button.hpp \ + src/messages/MessageContainer.hpp RESOURCES += \ resources/resources.qrc \ + resources/resources_autogenerated.qrc DISTFILES += @@ -450,7 +484,6 @@ win32-msvc* { QMAKE_CXXFLAGS_WARN_ON += -Wno-deprecated-declarations QMAKE_CXXFLAGS_WARN_ON += -Wno-sign-compare QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-variable - QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-private-field # Disabling strict-aliasing warnings for now, although we probably want to re-enable this in the future QMAKE_CXXFLAGS_WARN_ON += -Wno-strict-aliasing @@ -459,6 +492,7 @@ win32-msvc* { equals(QMAKE_CXX, "clang++") { QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-local-typedef + QMAKE_CXXFLAGS_WARN_ON += -Wno-unused-private-field } else { QMAKE_CXXFLAGS_WARN_ON += -Wno-class-memaccess } diff --git a/lib/settings b/lib/settings index 29accdf9d..7f0db95f2 160000 --- a/lib/settings +++ b/lib/settings @@ -1 +1 @@ -Subproject commit 29accdf9dea05947d687112594ad06bf6001ee0a +Subproject commit 7f0db95f245fb726e756ecde15a800c0928b054b diff --git a/lib/signals b/lib/signals index 3f6645c61..e03c868ec 160000 --- a/lib/signals +++ b/lib/signals @@ -1 +1 @@ -Subproject commit 3f6645c615ff7bf412c05fe322e589cbdd34ff9b +Subproject commit e03c868ec922027a0e672b64388808beb1297816 diff --git a/resources/__pycache__/_generate_resources.cpython-36.pyc b/resources/__pycache__/_generate_resources.cpython-36.pyc new file mode 100644 index 000000000..dfaf6af51 Binary files /dev/null and b/resources/__pycache__/_generate_resources.cpython-36.pyc differ diff --git a/resources/_generate_resources.py b/resources/_generate_resources.py new file mode 100644 index 000000000..2ce917fbd --- /dev/null +++ b/resources/_generate_resources.py @@ -0,0 +1,38 @@ +resources_header = \ +''' + ''' + +resources_footer = \ +''' +''' + +header_header = \ +'''#include +#include "common/Singleton.hpp" + +namespace chatterino { + +class Resources2 : public Singleton { +public: + Resources2(); + +''' + +header_footer = \ +'''}; + +} // namespace chatterino''' + +source_header = \ +'''#include "ResourcesAutogen.hpp" + +namespace chatterino { + +Resources2::Resources2() +{ +''' + +source_footer = \ +'''} + +} // namespace chatterino''' diff --git a/resources/images/button_ban.png b/resources/buttons/ban.png similarity index 100% rename from resources/images/button_ban.png rename to resources/buttons/ban.png diff --git a/resources/images/buttons/ban.png b/resources/buttons/banRed.png similarity index 100% rename from resources/images/buttons/ban.png rename to resources/buttons/banRed.png diff --git a/resources/images/emote.svg b/resources/buttons/emote.svg similarity index 100% rename from resources/images/emote.svg rename to resources/buttons/emote.svg diff --git a/resources/images/emote_dark.svg b/resources/buttons/emoteDark.svg similarity index 100% rename from resources/images/emote_dark.svg rename to resources/buttons/emoteDark.svg diff --git a/resources/images/menu_black.png b/resources/buttons/menuDark.png similarity index 100% rename from resources/images/menu_black.png rename to resources/buttons/menuDark.png diff --git a/resources/images/menu_white.png b/resources/buttons/menuLight.png similarity index 100% rename from resources/images/menu_white.png rename to resources/buttons/menuLight.png diff --git a/resources/images/buttons/mod.png b/resources/buttons/mod.png similarity index 100% rename from resources/images/buttons/mod.png rename to resources/buttons/mod.png diff --git a/resources/images/moderatormode_disabled.png b/resources/buttons/modModeDisabled.png similarity index 100% rename from resources/images/moderatormode_disabled.png rename to resources/buttons/modModeDisabled.png diff --git a/resources/images/moderatormode_disabled2.png b/resources/buttons/modModeDisabled2.png similarity index 100% rename from resources/images/moderatormode_disabled2.png rename to resources/buttons/modModeDisabled2.png diff --git a/resources/images/moderatormode_enabled.png b/resources/buttons/modModeEnabled.png similarity index 100% rename from resources/images/moderatormode_enabled.png rename to resources/buttons/modModeEnabled.png diff --git a/resources/images/moderatormode_enabled2.png b/resources/buttons/modModeEnabled2.png similarity index 100% rename from resources/images/moderatormode_enabled2.png rename to resources/buttons/modModeEnabled2.png diff --git a/resources/images/button_timeout.png b/resources/buttons/timeout.png similarity index 100% rename from resources/images/button_timeout.png rename to resources/buttons/timeout.png diff --git a/resources/images/buttons/unban.png b/resources/buttons/unban.png similarity index 100% rename from resources/images/buttons/unban.png rename to resources/buttons/unban.png diff --git a/resources/images/buttons/unmod.png b/resources/buttons/unmod.png similarity index 100% rename from resources/images/buttons/unmod.png rename to resources/buttons/unmod.png diff --git a/resources/images/download_update.png b/resources/buttons/update.png similarity index 100% rename from resources/images/download_update.png rename to resources/buttons/update.png diff --git a/resources/images/download_update_error.png b/resources/buttons/updateError.png similarity index 100% rename from resources/images/download_update_error.png rename to resources/buttons/updateError.png diff --git a/resources/images/chatterino2.icns b/resources/chatterino2.icns similarity index 100% rename from resources/images/chatterino2.icns rename to resources/chatterino2.icns diff --git a/resources/error.png b/resources/error.png new file mode 100644 index 000000000..07fba9f7c Binary files /dev/null and b/resources/error.png differ diff --git a/resources/generate_resources.py b/resources/generate_resources.py new file mode 100755 index 000000000..4b047219c --- /dev/null +++ b/resources/generate_resources.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +from pathlib import Path + +from _generate_resources import * + +ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'windows.rc', + 'generate_resources.py', '_generate_resources.py'] +ignored_directories = ['__pycache__'] + +def isNotIgnored(file): + return str(file) not in ignored_files + +all_files = list(filter(isNotIgnored, \ + filter(Path.is_file, Path('.').glob('**/*')))) +image_files = list(filter(isNotIgnored, \ + filter(Path.is_file, Path('.').glob('**/*.png')))) + +with open('./resources_autogenerated.qrc', 'w') as out: + out.write(resources_header) + for file in all_files: + out.write(f" {str(file)}\n") + out.write(resources_footer) + +with open('../src/autogenerated/ResourcesAutogen.cpp', 'w') as out: + out.write(source_header) + for file in sorted(image_files): + var_name = str(file.with_suffix("")).replace("/",".") + out.write(f' this->{var_name}') + out.write(f' = QPixmap(":/{file}");\n') + out.write(source_footer) + +def writeHeader(out, name, element, indent): + if isinstance(element, dict): + if name != "": + out.write(f"{indent}struct {{\n") + for (key, value) in element.items(): + writeHeader(out, key, value, indent + ' ') + if name != "": + out.write(f"{indent}}} {name};\n"); + else: + out.write(f"{indent}QPixmap {element};\n") + +with open('../src/autogenerated/ResourcesAutogen.hpp', 'w') as out: + out.write(header_header) + + elements = {} + for file in sorted(image_files): + elements_ref = elements + directories = str(file).split('/')[:-1] + filename = file.stem + for directory in directories: + if directory not in elements_ref: + if directory not in ignored_directories: + elements_ref[directory] = {} + elements_ref = elements_ref[directory] + elements_ref[filename] = filename + + writeHeader(out, "", elements, '') + + out.write(header_footer) + diff --git a/resources/images/icon.png b/resources/icon.png similarity index 100% rename from resources/images/icon.png rename to resources/icon.png diff --git a/resources/images/AppearanceEditorPart_16x.png b/resources/images/AppearanceEditorPart_16x.png deleted file mode 100644 index 86c59f7ff..000000000 Binary files a/resources/images/AppearanceEditorPart_16x.png and /dev/null differ diff --git a/resources/images/BrowserLink_16x.png b/resources/images/BrowserLink_16x.png deleted file mode 100644 index bafb9d4e7..000000000 Binary files a/resources/images/BrowserLink_16x.png and /dev/null differ diff --git a/resources/images/CopyLongTextToClipboard_16x.png b/resources/images/CopyLongTextToClipboard_16x.png deleted file mode 100644 index 774ee97fa..000000000 Binary files a/resources/images/CopyLongTextToClipboard_16x.png and /dev/null differ diff --git a/resources/images/CustomActionEditor_16x.png b/resources/images/CustomActionEditor_16x.png deleted file mode 100644 index b7b68a21e..000000000 Binary files a/resources/images/CustomActionEditor_16x.png and /dev/null differ diff --git a/resources/images/Emoji_Color_1F60A_19 old.png b/resources/images/Emoji_Color_1F60A_19 old.png deleted file mode 100644 index 81d296bf8..000000000 Binary files a/resources/images/Emoji_Color_1F60A_19 old.png and /dev/null differ diff --git a/resources/images/Emoji_Color_1F60A_19.png b/resources/images/Emoji_Color_1F60A_19.png deleted file mode 100644 index 15e15bd4a..000000000 Binary files a/resources/images/Emoji_Color_1F60A_19.png and /dev/null differ diff --git a/resources/images/Filter_16x.png b/resources/images/Filter_16x.png deleted file mode 100644 index f946e63c1..000000000 Binary files a/resources/images/Filter_16x.png and /dev/null differ diff --git a/resources/images/Message_16xLG.png b/resources/images/Message_16xLG.png deleted file mode 100644 index 7d06b1995..000000000 Binary files a/resources/images/Message_16xLG.png and /dev/null differ diff --git a/resources/images/StatusAnnotations_Blocked_16xLG_color.png b/resources/images/StatusAnnotations_Blocked_16xLG_color.png deleted file mode 100644 index b58166e30..000000000 Binary files a/resources/images/StatusAnnotations_Blocked_16xLG_color.png and /dev/null differ diff --git a/resources/images/UserProfile_22x.png b/resources/images/UserProfile_22x.png deleted file mode 100644 index c47f61243..000000000 Binary files a/resources/images/UserProfile_22x.png and /dev/null differ diff --git a/resources/images/VSO_Link_blue_16x.png b/resources/images/VSO_Link_blue_16x.png deleted file mode 100644 index eb3882929..000000000 Binary files a/resources/images/VSO_Link_blue_16x.png and /dev/null differ diff --git a/resources/images/cheer100.png b/resources/images/cheer100.png deleted file mode 100644 index 301998040..000000000 Binary files a/resources/images/cheer100.png and /dev/null differ diff --git a/resources/images/cheer1000.png b/resources/images/cheer1000.png deleted file mode 100644 index d6c0ed04a..000000000 Binary files a/resources/images/cheer1000.png and /dev/null differ diff --git a/resources/images/cheer10000.png b/resources/images/cheer10000.png deleted file mode 100644 index 3e3eb4284..000000000 Binary files a/resources/images/cheer10000.png and /dev/null differ diff --git a/resources/images/cheer100000.png b/resources/images/cheer100000.png deleted file mode 100644 index c20e956b1..000000000 Binary files a/resources/images/cheer100000.png and /dev/null differ diff --git a/resources/images/cheer5000.png b/resources/images/cheer5000.png deleted file mode 100644 index 89a7a4015..000000000 Binary files a/resources/images/cheer5000.png and /dev/null differ diff --git a/resources/images/collapse.png b/resources/images/collapse.png deleted file mode 100644 index b181ccbf9..000000000 Binary files a/resources/images/collapse.png and /dev/null differ diff --git a/resources/images/format_Bold_16xLG.png b/resources/images/format_Bold_16xLG.png deleted file mode 100644 index e636bb0d5..000000000 Binary files a/resources/images/format_Bold_16xLG.png and /dev/null differ diff --git a/resources/images/settings.png b/resources/images/settings.png deleted file mode 100644 index f6542bd48..000000000 Binary files a/resources/images/settings.png and /dev/null differ diff --git a/resources/images/tool_moreCollapser_off16.png b/resources/images/tool_moreCollapser_off16.png deleted file mode 100644 index 717fc75e2..000000000 Binary files a/resources/images/tool_moreCollapser_off16.png and /dev/null differ diff --git a/resources/licenses/emoji-data-source.txt b/resources/licenses/emoji-data-source.txt new file mode 100644 index 000000000..85ddb3c0f --- /dev/null +++ b/resources/licenses/emoji-data-source.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Cal Henderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/resources/pajaDank.png b/resources/pajaDank.png new file mode 100644 index 000000000..c8746932b Binary files /dev/null and b/resources/pajaDank.png differ diff --git a/resources/resources.qrc b/resources/resources.qrc index cf41c3469..8568b5032 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -1,81 +1,5 @@ - - images/AppearanceEditorPart_16x.png - images/BrowserLink_16x.png - images/cheer1.png - images/cheer100.png - images/cheer1000.png - images/cheer10000.png - images/cheer100000.png - images/cheer5000.png - images/verified.png - images/CopyLongTextToClipboard_16x.png - images/CustomActionEditor_16x.png - images/Emoji_Color_1F60A_19.png - images/Filter_16x.png - images/format_Bold_16xLG.png - images/Message_16xLG.png - images/settings.png - images/tool_moreCollapser_off16.png - images/twitchprime_bg.png - qss/settings.qss - images/admin_bg.png - images/broadcaster_bg.png - images/globalmod_bg.png - images/moderator_bg.png - images/staff_bg.png - images/turbo_bg.png - emojidata.txt - images/button_ban.png - images/button_timeout.png - images/StatusAnnotations_Blocked_16xLG_color.png - images/UserProfile_22x.png - images/VSO_Link_blue_16x.png - sounds/ping2.wav - images/subscriber.png - images/collapse.png - images/emote.svg - images/notifications.svg - images/behave.svg - images/theme.svg - images/accounts.svg - images/chatterino2.icns - images/icon.png - images/commands.svg - images/aboutlogo.png - images/about.svg - images/moderatormode_disabled.png - images/moderatormode_enabled.png - images/split/splitdown.png - images/split/splitleft.png - images/split/splitright.png - images/split/splitup.png - images/split/splitmove.png - licenses/boost_boost.txt - licenses/fmt_bsd2.txt - licenses/libcommuni_BSD3.txt - licenses/openssl.txt - licenses/pajlada_settings.txt - licenses/pajlada_signals.txt - licenses/qt_lgpl-3.0.txt - licenses/rapidjson.txt - licenses/websocketpp.txt - emoji.json - images/buttons/ban.png - images/buttons/mod.png - images/buttons/unban.png - images/buttons/unmod.png - images/emote_dark.svg - tlds.txt - images/menu_black.png - images/menu_white.png - contributors.txt - avatars/fourtf.png - avatars/pajlada.png - images/download_update.png - images/download_update_error.png - - - qt.conf - + + qt.conf + diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc new file mode 100644 index 000000000..6fc9e6f68 --- /dev/null +++ b/resources/resources_autogenerated.qrc @@ -0,0 +1,64 @@ + + pajaDank.png + icon.png + emojidata.txt + contributors.txt + error.png + emoji.json + icon.ico + tlds.txt + chatterino2.icns + qss/settings.qss + __pycache__/_generate_resources.cpython-36.pyc + licenses/fmt_bsd2.txt + licenses/openssl.txt + licenses/pajlada_settings.txt + licenses/qt_lgpl-3.0.txt + licenses/pajlada_signals.txt + licenses/rapidjson.txt + licenses/websocketpp.txt + licenses/boost_boost.txt + licenses/libcommuni_BSD3.txt + settings/aboutlogo.png + settings/behave.svg + settings/accounts.svg + settings/about.svg + settings/notifications.svg + settings/commands.svg + settings/theme.svg + split/up.png + split/left.png + split/move.png + split/right.png + split/down.png + buttons/unban.png + buttons/menuDark.png + buttons/mod.png + buttons/emote.svg + buttons/modModeEnabled2.png + buttons/ban.png + buttons/unmod.png + buttons/emoteDark.svg + buttons/updateError.png + buttons/modModeDisabled.png + buttons/modModeDisabled2.png + buttons/modModeEnabled.png + buttons/menuLight.png + buttons/update.png + buttons/timeout.png + buttons/banRed.png + sounds/ping2.wav + twitch/prime.png + twitch/verified.png + twitch/admin.png + twitch/subscriber.png + twitch/turbo.png + twitch/moderator.png + twitch/globalmod.png + twitch/cheer1.png + twitch/broadcaster.png + twitch/staff.png + avatars/fourtf.png + avatars/pajlada.png + + \ No newline at end of file diff --git a/resources/images/about.svg b/resources/settings/about.svg similarity index 100% rename from resources/images/about.svg rename to resources/settings/about.svg diff --git a/resources/images/aboutlogo.png b/resources/settings/aboutlogo.png similarity index 100% rename from resources/images/aboutlogo.png rename to resources/settings/aboutlogo.png diff --git a/resources/images/accounts.svg b/resources/settings/accounts.svg similarity index 100% rename from resources/images/accounts.svg rename to resources/settings/accounts.svg diff --git a/resources/images/behave.svg b/resources/settings/behave.svg similarity index 100% rename from resources/images/behave.svg rename to resources/settings/behave.svg diff --git a/resources/images/commands.svg b/resources/settings/commands.svg similarity index 100% rename from resources/images/commands.svg rename to resources/settings/commands.svg diff --git a/resources/settings/emote.svg b/resources/settings/emote.svg new file mode 100644 index 000000000..10e25c9f5 --- /dev/null +++ b/resources/settings/emote.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/resources/images/notifications.svg b/resources/settings/notifications.svg similarity index 100% rename from resources/images/notifications.svg rename to resources/settings/notifications.svg diff --git a/resources/images/theme.svg b/resources/settings/theme.svg similarity index 100% rename from resources/images/theme.svg rename to resources/settings/theme.svg diff --git a/resources/images/split/splitdown.png b/resources/split/down.png similarity index 100% rename from resources/images/split/splitdown.png rename to resources/split/down.png diff --git a/resources/images/split/splitleft.png b/resources/split/left.png similarity index 100% rename from resources/images/split/splitleft.png rename to resources/split/left.png diff --git a/resources/images/split/splitmove.png b/resources/split/move.png similarity index 100% rename from resources/images/split/splitmove.png rename to resources/split/move.png diff --git a/resources/images/split/splitright.png b/resources/split/right.png similarity index 100% rename from resources/images/split/splitright.png rename to resources/split/right.png diff --git a/resources/images/split/splitup.png b/resources/split/up.png similarity index 100% rename from resources/images/split/splitup.png rename to resources/split/up.png diff --git a/resources/images/admin_bg.png b/resources/twitch/admin.png similarity index 100% rename from resources/images/admin_bg.png rename to resources/twitch/admin.png diff --git a/resources/images/broadcaster_bg.png b/resources/twitch/broadcaster.png similarity index 100% rename from resources/images/broadcaster_bg.png rename to resources/twitch/broadcaster.png diff --git a/resources/images/cheer1.png b/resources/twitch/cheer1.png similarity index 100% rename from resources/images/cheer1.png rename to resources/twitch/cheer1.png diff --git a/resources/images/globalmod_bg.png b/resources/twitch/globalmod.png similarity index 100% rename from resources/images/globalmod_bg.png rename to resources/twitch/globalmod.png diff --git a/resources/images/moderator_bg.png b/resources/twitch/moderator.png similarity index 100% rename from resources/images/moderator_bg.png rename to resources/twitch/moderator.png diff --git a/resources/images/twitchprime_bg.png b/resources/twitch/prime.png similarity index 100% rename from resources/images/twitchprime_bg.png rename to resources/twitch/prime.png diff --git a/resources/images/staff_bg.png b/resources/twitch/staff.png similarity index 100% rename from resources/images/staff_bg.png rename to resources/twitch/staff.png diff --git a/resources/images/subscriber.png b/resources/twitch/subscriber.png similarity index 100% rename from resources/images/subscriber.png rename to resources/twitch/subscriber.png diff --git a/resources/images/turbo_bg.png b/resources/twitch/turbo.png similarity index 100% rename from resources/images/turbo_bg.png rename to resources/twitch/turbo.png diff --git a/resources/images/verified.png b/resources/twitch/verified.png similarity index 100% rename from resources/images/verified.png rename to resources/twitch/verified.png diff --git a/src/Application.cpp b/src/Application.cpp index 9068bf9a8..85b4f382c 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -6,9 +6,11 @@ #include "controllers/ignores/IgnoreController.hpp" #include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/taggedusers/TaggedUsersController.hpp" +#include "messages/MessageBuilder.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchServer.hpp" -#include "singletons/Emotes.hpp" #include "singletons/Fonts.hpp" #include "singletons/Logging.hpp" #include "singletons/NativeMessaging.hpp" @@ -24,69 +26,77 @@ namespace chatterino { -static std::atomic isAppConstructed{false}; static std::atomic isAppInitialized{false}; -static Application *staticApp = nullptr; +Application *Application::instance = nullptr; // this class is responsible for handling the workflow of Chatterino -// It will create the instances of the major classes, and connect their signals to each other +// It will create the instances of the major classes, and connect their signals +// to each other -Application::Application(int _argc, char **_argv) - : argc_(_argc) - , argv_(_argv) +Application::Application(Settings &_settings, Paths &_paths) + : settings(&_settings) + , paths(&_paths) + , resources(&this->emplace()) + + , themes(&this->emplace()) + , fonts(&this->emplace()) + , emotes(&this->emplace()) + , windows(&this->emplace()) + + , accounts(&this->emplace()) + , commands(&this->emplace()) + , highlights(&this->emplace()) + , ignores(&this->emplace()) + , taggedUsers(&this->emplace()) + , moderationActions(&this->emplace()) + , twitch2(&this->emplace()) + , logging(&this->emplace()) { - getSettings()->initialize(); - getSettings()->load(); -} + this->instance = this; -void Application::construct() -{ - assert(isAppConstructed == false); - isAppConstructed = true; + this->fonts->fontChanged.connect( + [this]() { this->windows->layoutChannelViews(); }); - // 1. Instantiate all classes - this->settings = getSettings(); - this->paths = getPaths(); - - this->addSingleton(this->themes = new Theme); - this->addSingleton(this->windows = new WindowManager); - this->addSingleton(this->logging = new Logging); - this->addSingleton(this->commands = new CommandController); - this->addSingleton(this->highlights = new HighlightController); - this->addSingleton(this->ignores = new IgnoreController); - this->addSingleton(this->taggedUsers = new TaggedUsersController); - this->addSingleton(this->accounts = new AccountController); - this->addSingleton(this->emotes = new Emotes); - this->addSingleton(this->fonts = new Fonts); - this->addSingleton(this->resources = new Resources); - this->addSingleton(this->moderationActions = new ModerationActions); - - this->addSingleton(this->twitch2 = new TwitchServer); this->twitch.server = this->twitch2; this->twitch.pubsub = this->twitch2->pubsub; } -void Application::instantiate(int argc, char **argv) -{ - assert(staticApp == nullptr); - - staticApp = new Application(argc, argv); -} - -void Application::initialize() +void Application::initialize(Settings &settings, Paths &paths) { assert(isAppInitialized == false); isAppInitialized = true; - // 2. Initialize/load classes - for (Singleton *singleton : this->singletons_) { - singleton->initialize(*this); + for (auto &singleton : this->singletons_) { + singleton->initialize(settings, paths); } - // XXX this->windows->updateWordTypeMask(); + this->initNm(); + this->initPubsub(); +} + +int Application::run(QApplication &qtApp) +{ + assert(isAppInitialized); + + this->twitch.server->connect(); + + this->windows->getMainWindow().show(); + + return qtApp.exec(); +} + +void Application::save() +{ + for (auto &singleton : this->singletons_) { + singleton->save(); + } +} + +void Application::initNm() +{ #ifdef Q_OS_WIN #ifdef QT_DEBUG #ifdef C_DEBUG_NM @@ -98,49 +108,61 @@ void Application::initialize() this->nativeMessaging->openGuiMessageQueue(); #endif #endif +} +void Application::initPubsub() +{ this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) { - Log("WHISPER SENT LOL"); // + log("WHISPER SENT LOL"); // }); this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) { - Log("WHISPER RECEIVED LOL"); // + log("WHISPER RECEIVED LOL"); // }); - this->twitch.pubsub->signals_.moderation.chatCleared.connect([this](const auto &action) { - auto chan = this->twitch.server->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) { - return; - } + this->twitch.pubsub->signals_.moderation.chatCleared.connect( + [this](const auto &action) { + auto chan = + this->twitch.server->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) { + return; + } - QString text = QString("%1 cleared the chat").arg(action.source.name); + QString text = + QString("%1 cleared the chat").arg(action.source.name); - auto msg = Message::createSystemMessage(text); - postToThread([chan, msg] { chan->addMessage(msg); }); - }); + auto msg = makeSystemMessage(text); + postToThread([chan, msg] { chan->addMessage(msg); }); + }); - this->twitch.pubsub->signals_.moderation.modeChanged.connect([this](const auto &action) { - auto chan = this->twitch.server->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) { - return; - } + this->twitch.pubsub->signals_.moderation.modeChanged.connect( + [this](const auto &action) { + auto chan = + this->twitch.server->getChannelOrEmptyByID(action.roomID); + if (chan->isEmpty()) { + return; + } - QString text = QString("%1 turned %2 %3 mode") // - .arg(action.source.name) - .arg(action.state == ModeChangedAction::State::On ? "on" : "off") - .arg(action.getModeName()); + QString text = + QString("%1 turned %2 %3 mode") // + .arg(action.source.name) + .arg(action.state == ModeChangedAction::State::On ? "on" + : "off") + .arg(action.getModeName()); - if (action.duration > 0) { - text.append(" (" + QString::number(action.duration) + " seconds)"); - } + if (action.duration > 0) { + text.append(" (" + QString::number(action.duration) + + " seconds)"); + } - auto msg = Message::createSystemMessage(text); - postToThread([chan, msg] { chan->addMessage(msg); }); - }); + auto msg = makeSystemMessage(text); + postToThread([chan, msg] { chan->addMessage(msg); }); + }); this->twitch.pubsub->signals_.moderation.moderationStateChanged.connect( [this](const auto &action) { - auto chan = this->twitch.server->getChannelOrEmptyByID(action.roomID); + auto chan = + this->twitch.server->getChannelOrEmptyByID(action.roomID); if (chan->isEmpty()) { return; } @@ -148,48 +170,57 @@ void Application::initialize() QString text; if (action.modded) { - text = QString("%1 modded %2").arg(action.source.name, action.target.name); + text = QString("%1 modded %2") + .arg(action.source.name, action.target.name); } else { - text = QString("%1 unmodded %2").arg(action.source.name, action.target.name); + text = QString("%1 unmodded %2") + .arg(action.source.name, action.target.name); } - auto msg = Message::createSystemMessage(text); + auto msg = makeSystemMessage(text); postToThread([chan, msg] { chan->addMessage(msg); }); }); - this->twitch.pubsub->signals_.moderation.userBanned.connect([&](const auto &action) { - auto chan = this->twitch.server->getChannelOrEmptyByID(action.roomID); + this->twitch.pubsub->signals_.moderation.userBanned.connect( + [&](const auto &action) { + auto chan = + this->twitch.server->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) { - return; - } + if (chan->isEmpty()) { + return; + } - auto msg = Message::createTimeoutMessage(action); - msg->flags |= Message::PubSub; + MessageBuilder msg(action); + msg->flags.set(MessageFlag::PubSub); - postToThread([chan, msg] { chan->addOrReplaceTimeout(msg); }); - }); + postToThread([chan, msg = msg.release()] { + chan->addOrReplaceTimeout(msg); + }); + }); - this->twitch.pubsub->signals_.moderation.userUnbanned.connect([&](const auto &action) { - auto chan = this->twitch.server->getChannelOrEmptyByID(action.roomID); + this->twitch.pubsub->signals_.moderation.userUnbanned.connect( + [&](const auto &action) { + auto chan = + this->twitch.server->getChannelOrEmptyByID(action.roomID); - if (chan->isEmpty()) { - return; - } + if (chan->isEmpty()) { + return; + } - auto msg = Message::createUntimeoutMessage(action); + auto msg = MessageBuilder(action).release(); - postToThread([chan, msg] { chan->addMessage(msg); }); - }); + postToThread([chan, msg] { chan->addMessage(msg); }); + }); this->twitch.pubsub->start(); auto RequestModerationActions = [=]() { this->twitch.server->pubsub->unlistenAllModerationActions(); - // TODO(pajlada): Unlisten to all authed topics instead of only moderation topics - // this->twitch.pubsub->UnlistenAllAuthedTopics(); + // TODO(pajlada): Unlisten to all authed topics instead of only + // moderation topics this->twitch.pubsub->UnlistenAllAuthedTopics(); - this->twitch.server->pubsub->listenToWhispers(this->accounts->twitch.getCurrent()); // + this->twitch.server->pubsub->listenToWhispers( + this->accounts->twitch.getCurrent()); // }; this->accounts->twitch.currentUserChanged.connect(RequestModerationActions); @@ -197,39 +228,11 @@ void Application::initialize() RequestModerationActions(); } -int Application::run(QApplication &qtApp) -{ - // Start connecting to the IRC Servers (Twitch only for now) - this->twitch.server->connect(); - - // Show main window - this->windows->getMainWindow().show(); - - return qtApp.exec(); -} - -void Application::save() -{ - for (Singleton *singleton : this->singletons_) { - singleton->save(); - } -} - -void Application::addSingleton(Singleton *singleton) -{ - this->singletons_.push_back(singleton); -} - Application *getApp() { - assert(staticApp != nullptr); + assert(Application::instance != nullptr); - return staticApp; -} - -bool appInitialized() -{ - return isAppInitialized; + return Application::instance; } } // namespace chatterino diff --git a/src/Application.hpp b/src/Application.hpp index 8fda2ebba..2a62503bf 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,13 +1,13 @@ #pragma once +#include "common/Singleton.hpp" #include "singletons/Resources.hpp" #include +#include namespace chatterino { -class Singleton; - class TwitchServer; class PubSub; @@ -24,68 +24,69 @@ class Logging; class Paths; class AccountManager; class Emotes; -class NativeMessaging; class Settings; class Fonts; class Resources; class Application { - Application(int _argc, char **_argv); + std::vector> singletons_; + int argc_; + char **argv_; public: - static void instantiate(int argc_, char **argv_); + static Application *instance; - ~Application() = delete; + Application(Settings &settings, Paths &paths); - void construct(); - void initialize(); + void initialize(Settings &settings, Paths &paths); void load(); + void save(); int run(QApplication &qtApp); friend void test(); - [[deprecated("use getSettings() instead")]] Settings *settings = nullptr; - [[deprecated("use getPaths() instead")]] Paths *paths = nullptr; + Settings *const settings{}; + Paths *const paths{}; + Resources2 *const resources; - Theme *themes = nullptr; - WindowManager *windows = nullptr; - Logging *logging = nullptr; - CommandController *commands = nullptr; - HighlightController *highlights = nullptr; - IgnoreController *ignores = nullptr; - TaggedUsersController *taggedUsers = nullptr; - AccountController *accounts = nullptr; - Emotes *emotes = nullptr; - NativeMessaging *nativeMessaging = nullptr; - Fonts *fonts = nullptr; - Resources *resources = nullptr; - ModerationActions *moderationActions = nullptr; - TwitchServer *twitch2 = nullptr; + Theme *const themes{}; + Fonts *const fonts{}; + Emotes *const emotes{}; + WindowManager *const windows{}; + + AccountController *const accounts{}; + CommandController *const commands{}; + HighlightController *const highlights{}; + IgnoreController *const ignores{}; + TaggedUsersController *const taggedUsers{}; + ModerationActions *const moderationActions{}; + TwitchServer *const twitch2{}; + + /*[[deprecated]]*/ Logging *const logging{}; /// Provider-specific struct { - [[deprecated("use twitch2 instead")]] TwitchServer *server = nullptr; - [[deprecated("use twitch2->pubsub instead")]] PubSub *pubsub = nullptr; + /*[[deprecated("use twitch2 instead")]]*/ TwitchServer *server{}; + /*[[deprecated("use twitch2->pubsub instead")]]*/ PubSub *pubsub{}; } twitch; - void save(); - - // Special application mode that only initializes the native messaging host - static void runNativeMessagingHost(); - private: void addSingleton(Singleton *singleton); + void initPubsub(); + void initNm(); - int argc_; - char **argv_; - - std::vector singletons_; + template ::value>> + T &emplace() + { + auto t = new T; + this->singletons_.push_back(std::unique_ptr(t)); + return *t; + } }; Application *getApp(); -bool appInitialized(); - } // namespace chatterino diff --git a/src/BrowserExtension.cpp b/src/BrowserExtension.cpp new file mode 100644 index 000000000..5275e0648 --- /dev/null +++ b/src/BrowserExtension.cpp @@ -0,0 +1,87 @@ +#include "BrowserExtension.hpp" + +#include "singletons/NativeMessaging.hpp" + +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#include +#endif + +namespace chatterino { + +namespace { +void initFileMode() +{ +#ifdef Q_OS_WIN + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif +} + +void runLoop(NativeMessagingClient &client) +{ + while (true) { + char size_c[4]; + std::cin.read(size_c, 4); + + if (std::cin.eof()) break; + + auto size = *reinterpret_cast(size_c); + +#if 0 + bool bigEndian = isBigEndian(); + // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead + uint32_t size = 0; + if (bigEndian) { + size = size_c[3] | static_cast(size_c[2]) << 8 | + static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; + } else { + size = size_c[0] | static_cast(size_c[1]) << 8 | + static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; + } +#endif + + std::unique_ptr buffer(new char[size + 1]); + std::cin.read(buffer.get(), size); + *(buffer.get() + size) = '\0'; + + client.sendMessage( + QByteArray::fromRawData(buffer.get(), static_cast(size))); + } +} +} // namespace + +bool shouldRunBrowserExtensionHost(const QStringList &args) +{ + return args.size() > 0 && (args[0].startsWith("chrome-extension://") || + args[0].endsWith(".json")); +} + +void runBrowserExtensionHost() +{ + initFileMode(); + + std::atomic ping(false); + + QTimer timer; + QObject::connect(&timer, &QTimer::timeout, [&ping] { + if (!ping.exchange(false)) { + _Exit(0); + } + }); + timer.setInterval(11000); + timer.start(); + + NativeMessagingClient client; + + runLoop(client); +} + +} // namespace chatterino diff --git a/src/BrowserExtension.hpp b/src/BrowserExtension.hpp new file mode 100644 index 000000000..0232ac2b8 --- /dev/null +++ b/src/BrowserExtension.hpp @@ -0,0 +1,10 @@ +#pragma once + +class QStringList; + +namespace chatterino { + +bool shouldRunBrowserExtensionHost(const QStringList &args); +void runBrowserExtensionHost(); + +} // namespace chatterino diff --git a/src/RunGui.cpp b/src/RunGui.cpp new file mode 100644 index 000000000..9fdcdb20c --- /dev/null +++ b/src/RunGui.cpp @@ -0,0 +1,136 @@ +#include "RunGui.hpp" + +#include +#include +#include +#include + +#include "Application.hpp" +#include "common/NetworkManager.hpp" +#include "singletons/Paths.hpp" +#include "singletons/Updates.hpp" +#include "widgets/dialogs/LastRunCrashDialog.hpp" + +#ifdef C_USE_BREAKPAD +#include +#endif + +// void initQt(); +// void installCustomPalette(); +// void showLastCrashDialog(); +// void createRunningFile(const QString &path); +// void removeRunningFile(const QString &path); + +namespace chatterino { +namespace { +void installCustomPalette() +{ + // borrowed from + // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows + auto dark = qApp->palette(); + + dark.setColor(QPalette::Window, QColor(22, 22, 22)); + dark.setColor(QPalette::WindowText, Qt::white); + dark.setColor(QPalette::Text, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::WindowText, + QColor(127, 127, 127)); + dark.setColor(QPalette::Base, QColor("#333")); + dark.setColor(QPalette::AlternateBase, QColor("#444")); + dark.setColor(QPalette::ToolTipBase, Qt::white); + dark.setColor(QPalette::ToolTipText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); + dark.setColor(QPalette::Dark, QColor(35, 35, 35)); + dark.setColor(QPalette::Shadow, QColor(20, 20, 20)); + dark.setColor(QPalette::Button, QColor(70, 70, 70)); + dark.setColor(QPalette::ButtonText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::ButtonText, + QColor(127, 127, 127)); + dark.setColor(QPalette::BrightText, Qt::red); + dark.setColor(QPalette::Link, QColor(42, 130, 218)); + dark.setColor(QPalette::Highlight, QColor(42, 130, 218)); + dark.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); + dark.setColor(QPalette::HighlightedText, Qt::white); + dark.setColor(QPalette::Disabled, QPalette::HighlightedText, + QColor(127, 127, 127)); + + qApp->setPalette(dark); +} + +void initQt() +{ + // set up the QApplication flags + QApplication::setAttribute(Qt::AA_Use96Dpi, true); +#ifdef Q_OS_WIN32 + QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); +#endif + + QApplication::setStyle(QStyleFactory::create("Fusion")); + + installCustomPalette(); +} + +void showLastCrashDialog() +{ +#ifndef C_DISABLE_CRASH_DIALOG + LastRunCrashDialog dialog; + + switch (dialog.exec()) { + case QDialog::Accepted: { + }; break; + default: { + _exit(0); + } + } +#endif +} + +void createRunningFile(const QString &path) +{ + QFile runningFile(path); + + runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); + runningFile.flush(); + runningFile.close(); +} + +void removeRunningFile(const QString &path) +{ + QFile::remove(path); +} +} // namespace + +void runGui(QApplication &a, Paths &paths, Settings &settings) +{ + initQt(); + + chatterino::NetworkManager::init(); + chatterino::Updates::getInstance().checkForUpdates(); + +#ifdef C_USE_BREAKPAD + QBreakpadInstance.setDumpPath(app->paths->settingsFolderPath + "/Crashes"); +#endif + + // Running file + auto runningPath = + paths.miscDirectory + "/running_" + paths.applicationFilePathHash; + + if (QFile::exists(runningPath)) { + showLastCrashDialog(); + } else { + createRunningFile(runningPath); + } + + Application app(settings, paths); + app.initialize(settings, paths); + app.run(a); + app.save(); + + removeRunningFile(runningPath); + + pajlada::Settings::SettingManager::gSave(); + + chatterino::NetworkManager::deinit(); + + _exit(0); +} +} // namespace chatterino diff --git a/src/RunGui.hpp b/src/RunGui.hpp new file mode 100644 index 000000000..338164404 --- /dev/null +++ b/src/RunGui.hpp @@ -0,0 +1,10 @@ +#pragma once + +class QApplication; + +namespace chatterino { +class Paths; +class Settings; + +void runGui(QApplication &a, Paths &paths, Settings &settings); +} // namespace chatterino diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp new file mode 100644 index 000000000..c5035f2e6 --- /dev/null +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -0,0 +1,44 @@ +#include "ResourcesAutogen.hpp" + +namespace chatterino { + +Resources2::Resources2() +{ + this->avatars.fourtf = QPixmap(":/avatars/fourtf.png"); + this->avatars.pajlada = QPixmap(":/avatars/pajlada.png"); + this->buttons.ban = QPixmap(":/buttons/ban.png"); + this->buttons.banRed = QPixmap(":/buttons/banRed.png"); + this->buttons.menuDark = QPixmap(":/buttons/menuDark.png"); + this->buttons.menuLight = QPixmap(":/buttons/menuLight.png"); + this->buttons.mod = QPixmap(":/buttons/mod.png"); + this->buttons.modModeDisabled = QPixmap(":/buttons/modModeDisabled.png"); + this->buttons.modModeDisabled2 = QPixmap(":/buttons/modModeDisabled2.png"); + this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png"); + this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png"); + this->buttons.timeout = QPixmap(":/buttons/timeout.png"); + this->buttons.unban = QPixmap(":/buttons/unban.png"); + this->buttons.unmod = QPixmap(":/buttons/unmod.png"); + this->buttons.update = QPixmap(":/buttons/update.png"); + this->buttons.updateError = QPixmap(":/buttons/updateError.png"); + this->error = QPixmap(":/error.png"); + this->icon = QPixmap(":/icon.png"); + this->pajaDank = QPixmap(":/pajaDank.png"); + this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png"); + this->split.down = QPixmap(":/split/down.png"); + this->split.left = QPixmap(":/split/left.png"); + this->split.move = QPixmap(":/split/move.png"); + this->split.right = QPixmap(":/split/right.png"); + this->split.up = QPixmap(":/split/up.png"); + this->twitch.admin = QPixmap(":/twitch/admin.png"); + this->twitch.broadcaster = QPixmap(":/twitch/broadcaster.png"); + this->twitch.cheer1 = QPixmap(":/twitch/cheer1.png"); + this->twitch.globalmod = QPixmap(":/twitch/globalmod.png"); + this->twitch.moderator = QPixmap(":/twitch/moderator.png"); + this->twitch.prime = QPixmap(":/twitch/prime.png"); + this->twitch.staff = QPixmap(":/twitch/staff.png"); + this->twitch.subscriber = QPixmap(":/twitch/subscriber.png"); + this->twitch.turbo = QPixmap(":/twitch/turbo.png"); + this->twitch.verified = QPixmap(":/twitch/verified.png"); +} + +} // namespace chatterino \ No newline at end of file diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp new file mode 100644 index 000000000..c6adb3576 --- /dev/null +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -0,0 +1,58 @@ +#include +#include "common/Singleton.hpp" + +namespace chatterino { + +class Resources2 : public Singleton +{ +public: + Resources2(); + + struct { + QPixmap fourtf; + QPixmap pajlada; + } avatars; + struct { + QPixmap ban; + QPixmap banRed; + QPixmap menuDark; + QPixmap menuLight; + QPixmap mod; + QPixmap modModeDisabled; + QPixmap modModeDisabled2; + QPixmap modModeEnabled; + QPixmap modModeEnabled2; + QPixmap timeout; + QPixmap unban; + QPixmap unmod; + QPixmap update; + QPixmap updateError; + } buttons; + QPixmap error; + QPixmap icon; + QPixmap pajaDank; + struct { + QPixmap aboutlogo; + } settings; + struct { + QPixmap down; + QPixmap left; + QPixmap move; + QPixmap right; + QPixmap up; + } split; + struct { + QPixmap admin; + QPixmap broadcaster; + QPixmap cheer1; + QPixmap globalmod; + QPixmap moderator; + QPixmap prime; + QPixmap staff; + QPixmap subscriber; + QPixmap turbo; + QPixmap verified; + } twitch; +}; + +} // namespace chatterino \ No newline at end of file diff --git a/src/common/Aliases.hpp b/src/common/Aliases.hpp new file mode 100644 index 000000000..8a61a9639 --- /dev/null +++ b/src/common/Aliases.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#define QStringAlias(name) \ + namespace chatterino { \ + struct name { \ + QString string; \ + bool operator==(const name &other) const \ + { \ + return this->string == other.string; \ + } \ + bool operator!=(const name &other) const \ + { \ + return this->string != other.string; \ + } \ + }; \ + } /* namespace chatterino */ \ + namespace std { \ + template <> \ + struct hash { \ + size_t operator()(const chatterino::name &s) const \ + { \ + return qHash(s.string); \ + } \ + }; \ + } /* namespace std */ + +QStringAlias(UserName); +QStringAlias(UserId); +QStringAlias(Url); +QStringAlias(Tooltip); diff --git a/src/common/MutexValue.hpp b/src/common/Atomic.hpp similarity index 66% rename from src/common/MutexValue.hpp rename to src/common/Atomic.hpp index 112257370..39d8119f6 100644 --- a/src/common/MutexValue.hpp +++ b/src/common/Atomic.hpp @@ -6,14 +6,14 @@ namespace chatterino { template -class MutexValue : boost::noncopyable +class Atomic : boost::noncopyable { public: - MutexValue() + Atomic() { } - MutexValue(T &&val) + Atomic(T &&val) : value_(val) { } @@ -32,6 +32,13 @@ public: this->value_ = val; } + void set(T &&val) + { + std::lock_guard guard(this->mutex_); + + this->value_ = std::move(val); + } + private: mutable std::mutex mutex_; T value_; diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 26d9e536e..d7f1659f4 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,6 +3,7 @@ #include "Application.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" #include "singletons/Emotes.hpp" #include "singletons/Logging.hpp" #include "singletons/WindowManager.hpp" @@ -17,14 +18,18 @@ namespace chatterino { -Channel::Channel(const QString &_name, Type type) - : name(_name) - , completionModel(this->name) +// +// Channel +// +Channel::Channel(const QString &name, Type type) + : completionModel(name) + , name_(name) , type_(type) { - QObject::connect(&this->clearCompletionModelTimer_, &QTimer::timeout, [this]() { - this->completionModel.clearExpiredStrings(); // - }); + QObject::connect(&this->clearCompletionModelTimer_, &QTimer::timeout, + [this]() { + this->completionModel.clearExpiredStrings(); // + }); this->clearCompletionModelTimer_.start(60 * 1000); } @@ -38,6 +43,11 @@ Channel::Type Channel::getType() const return this->type_; } +const QString &Channel::getName() const +{ + return this->name_; +} + bool Channel::isTwitchChannel() const { return this->type_ >= Type::Twitch && this->type_ < Type::TwitchEnd; @@ -45,7 +55,7 @@ bool Channel::isTwitchChannel() const bool Channel::isEmpty() const { - return this->name.isEmpty(); + return this->name_.isEmpty(); } LimitedQueueSnapshot Channel::getMessageSnapshot() @@ -60,13 +70,13 @@ void Channel::addMessage(MessagePtr message) const QString &username = message->loginName; if (!username.isEmpty()) { - // TODO: Add recent chatters display name. This should maybe be a setting + // TODO: Add recent chatters display name this->addRecentChatter(message); } // FOURTF: change this when adding more providers if (this->isTwitchChannel()) { - app->logging->addMessage(this->name, message); + app->logging->addMessage(this->name_, message); } if (this->messages_.pushBack(message, deleted)) { @@ -90,37 +100,43 @@ void Channel::addOrReplaceTimeout(MessagePtr message) for (int i = snapshotLength - 1; i >= end; --i) { auto &s = snapshot[i]; - qDebug() << s->parseTime << minimumTime; - if (s->parseTime < minimumTime) { break; } - if (s->flags.HasFlag(Message::Untimeout) && s->timeoutUser == message->timeoutUser) { + if (s->flags.has(MessageFlag::Untimeout) && + s->timeoutUser == message->timeoutUser) { break; } - if (s->flags.HasFlag(Message::Timeout) && s->timeoutUser == message->timeoutUser) { - if (message->flags.HasFlag(Message::PubSub) && !s->flags.HasFlag(Message::PubSub)) { + if (s->flags.has(MessageFlag::Timeout) && + s->timeoutUser == message->timeoutUser) // + { + if (message->flags.has(MessageFlag::PubSub) && + !s->flags.has(MessageFlag::PubSub)) // + { this->replaceMessage(s, message); addMessage = false; break; } - if (!message->flags.HasFlag(Message::PubSub) && s->flags.HasFlag(Message::PubSub)) { + if (!message->flags.has(MessageFlag::PubSub) && + s->flags.has(MessageFlag::PubSub)) // + { addMessage = false; break; } int count = s->count + 1; - MessagePtr replacement(Message::createSystemMessage( - message->searchText + QString(" (") + QString::number(count) + " times)")); + MessageBuilder replacement(systemMessage, + message->searchText + QString(" (") + + QString::number(count) + " times)"); replacement->timeoutUser = message->timeoutUser; replacement->count = count; replacement->flags = message->flags; - this->replaceMessage(s, replacement); + this->replaceMessage(s, replacement.release()); return; } @@ -129,9 +145,10 @@ void Channel::addOrReplaceTimeout(MessagePtr message) // disable the messages from the user for (int i = 0; i < snapshotLength; i++) { auto &s = snapshot[i]; - if ((s->flags & (Message::Timeout | Message::Untimeout)) == 0 && + if (s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout}) && s->loginName == message->timeoutUser) { - s->flags.EnableFlag(Message::Disabled); + // FOURTF: disabled for now + // s->flags.EnableFlag(MessageFlag::Disabled); } } @@ -149,17 +166,19 @@ void Channel::disableAllMessages() int snapshotLength = snapshot.getLength(); for (int i = 0; i < snapshotLength; i++) { auto &s = snapshot[i]; - if (s->flags & Message::System || s->flags & Message::Timeout) { + if (s->flags.hasAny({MessageFlag::System, MessageFlag::Timeout})) { continue; } - s->flags.EnableFlag(Message::Disabled); + // FOURTF: disabled for now + // s->flags.EnableFlag(MessageFlag::Disabled); } } void Channel::addMessagesAtStart(std::vector &_messages) { - std::vector addedMessages = this->messages_.pushFront(_messages); + std::vector addedMessages = + this->messages_.pushFront(_messages); if (addedMessages.size() != 0) { this->messagesAddedAtStart.invoke(addedMessages); @@ -175,9 +194,8 @@ void Channel::replaceMessage(MessagePtr message, MessagePtr replacement) } } -void Channel::addRecentChatter(const std::shared_ptr &message) +void Channel::addRecentChatter(const MessagePtr &message) { - // Do nothing by default } bool Channel::canSendMessage() const @@ -215,4 +233,45 @@ void Channel::onConnected() { } +// +// Indirect channel +// +IndirectChannel::Data::Data(ChannelPtr _channel, Channel::Type _type) + : channel(_channel) + , type(_type) +{ +} + +IndirectChannel::IndirectChannel(ChannelPtr channel, Channel::Type type) + : data_(std::make_unique(channel, type)) +{ +} + +ChannelPtr IndirectChannel::get() +{ + return data_->channel; +} + +void IndirectChannel::reset(ChannelPtr channel) +{ + assert(this->data_->type != Channel::Type::Direct); + + this->data_->channel = channel; + this->data_->changed.invoke(); +} + +pajlada::Signals::NoArgSignal &IndirectChannel::getChannelChanged() +{ + return this->data_->changed; +} + +Channel::Type IndirectChannel::getType() +{ + if (this->data_->type == Channel::Type::Direct) { + return this->get()->getType(); + } else { + return this->data_->type; + } +} + } // namespace chatterino diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 552349419..cb93c1694 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -4,7 +4,6 @@ #include "messages/Image.hpp" #include "messages/LimitedQueue.hpp" #include "messages/Message.hpp" -#include "util/ConcurrentMap.hpp" #include #include @@ -32,7 +31,8 @@ public: explicit Channel(const QString &name, Type type); virtual ~Channel(); - pajlada::Signals::Signal sendMessageSignal; + pajlada::Signals::Signal + sendMessageSignal; pajlada::Signals::Signal messageRemovedFromStart; pajlada::Signals::Signal messageAppended; @@ -41,6 +41,7 @@ public: pajlada::Signals::NoArgSignal destroyed; Type getType() const; + const QString &getName() const; bool isTwitchChannel() const; virtual bool isEmpty() const; LimitedQueueSnapshot getMessageSnapshot(); @@ -50,9 +51,8 @@ public: void addOrReplaceTimeout(MessagePtr message); void disableAllMessages(); void replaceMessage(MessagePtr message, MessagePtr replacement); - virtual void addRecentChatter(const std::shared_ptr &message); + virtual void addRecentChatter(const MessagePtr &message); - QString name; QStringList modList; virtual bool canSendMessage() const; @@ -69,6 +69,7 @@ protected: virtual void onConnected(); private: + const QString name_; LimitedQueue messages_; Type type_; QTimer clearCompletionModelTimer_; @@ -83,46 +84,17 @@ class IndirectChannel Channel::Type type; pajlada::Signals::NoArgSignal changed; - Data() = delete; - Data(ChannelPtr _channel, Channel::Type _type) - : channel(_channel) - , type(_type) - { - } + Data(ChannelPtr channel, Channel::Type type); }; public: - IndirectChannel(ChannelPtr channel, Channel::Type type = Channel::Type::Direct) - : data_(new Data(channel, type)) - { - } + IndirectChannel(ChannelPtr channel, + Channel::Type type = Channel::Type::Direct); - ChannelPtr get() - { - return data_->channel; - } - - void update(ChannelPtr ptr) - { - assert(this->data_->type != Channel::Type::Direct); - - this->data_->channel = ptr; - this->data_->changed.invoke(); - } - - pajlada::Signals::NoArgSignal &getChannelChanged() - { - return this->data_->changed; - } - - Channel::Type getType() - { - if (this->data_->type == Channel::Type::Direct) { - return this->get()->getType(); - } else { - return this->data_->type; - } - } + ChannelPtr get(); + void reset(ChannelPtr channel); + pajlada::Signals::NoArgSignal &getChannelChanged(); + Channel::Type getType(); private: std::shared_ptr data_; diff --git a/src/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp index 226610381..370f58414 100644 --- a/src/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -41,7 +41,7 @@ public: using pajlada::Settings::Setting::operator==; using pajlada::Settings::Setting::operator!=; - using pajlada::Settings::Setting::operator const Type; + using pajlada::Settings::Setting::operator Type; }; using BoolSetting = ChatterinoSetting; diff --git a/src/common/Common.hpp b/src/common/Common.hpp index f73ea030b..251727c24 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -1,9 +1,13 @@ #pragma once +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" +#include "common/ProviderId.hpp" #include "debug/Log.hpp" #include #include +#include #include #include @@ -21,20 +25,18 @@ inline QString qS(const std::string &string) return QString::fromStdString(string); } -const Qt::KeyboardModifiers showSplitOverlayModifiers = Qt::ControlModifier | Qt::AltModifier; -const Qt::KeyboardModifiers showAddSplitRegions = Qt::ControlModifier | Qt::AltModifier; +const Qt::KeyboardModifiers showSplitOverlayModifiers = + Qt::ControlModifier | Qt::AltModifier; +const Qt::KeyboardModifiers showAddSplitRegions = + Qt::ControlModifier | Qt::AltModifier; const Qt::KeyboardModifiers showResizeHandlesModifiers = Qt::ControlModifier; static const char *ANONYMOUS_USERNAME_LABEL ATTR_UNUSED = " - anonymous - "; -#define return_if(x) \ - if ((x)) { \ - return; \ - } - -#define return_unless(x) \ - if (!(x)) { \ - return; \ - } +template +std::weak_ptr weakOf(T *element) +{ + return element->shared_from_this(); +} } // namespace chatterino diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index de5bafce8..9e2f4d024 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -2,8 +2,10 @@ #include "Application.hpp" #include "common/Common.hpp" +#include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" #include "debug/Log.hpp" +#include "providers/twitch/TwitchServer.hpp" #include "singletons/Emotes.hpp" #include @@ -102,46 +104,57 @@ int CompletionModel::rowCount(const QModelIndex &) const void CompletionModel::refresh() { - Log("[CompletionModel:{}] Refreshing...]", this->channelName_); + log("[CompletionModel:{}] Refreshing...]", this->channelName_); auto app = getApp(); // User-specific: Twitch Emotes - // TODO: Fix this so it properly updates with the proper api. oauth token needs proper scope - for (const auto &m : app->emotes->twitch.emotes) { - for (const auto &emoteName : m.second.emoteCodes) { - // XXX: No way to discern between a twitch global emote and sub emote right now - this->addString(emoteName, TaggedString::Type::TwitchGlobalEmote); + if (auto account = app->accounts->twitch.getCurrent()) { + for (const auto &emote : account->accessEmotes()->allEmoteNames) { + // XXX: No way to discern between a twitch global emote and sub + // emote right now + this->addString(emote.string, + TaggedString::Type::TwitchGlobalEmote); } } - // Global: BTTV Global Emotes - std::vector &bttvGlobalEmoteCodes = app->emotes->bttv.globalEmoteCodes; - for (const auto &m : bttvGlobalEmoteCodes) { - this->addString(m, TaggedString::Type::BTTVGlobalEmote); + // // Global: BTTV Global Emotes + // std::vector &bttvGlobalEmoteCodes = + // app->emotes->bttv.globalEmoteNames_; for (const auto &m : + // bttvGlobalEmoteCodes) { + // this->addString(m, TaggedString::Type::BTTVGlobalEmote); + // } + + // // Global: FFZ Global Emotes + // std::vector &ffzGlobalEmoteCodes = + // app->emotes->ffz.globalEmoteCodes; for (const auto &m : + // ffzGlobalEmoteCodes) { + // this->addString(m, TaggedString::Type::FFZGlobalEmote); + // } + + // Channel emotes + if (auto channel = dynamic_cast( + getApp() + ->twitch2->getChannelOrEmptyByID(this->channelName_) + .get())) { + auto bttv = channel->bttvEmotes(); + // auto it = bttv->begin(); + // for (const auto &emote : *bttv) { + // } + // std::vector &bttvChannelEmoteCodes = + // app->emotes->bttv.channelEmoteName_[this->channelName_]; + // for (const auto &m : bttvChannelEmoteCodes) { + // this->addString(m, TaggedString::Type::BTTVChannelEmote); + // } + + // Channel-specific: FFZ Channel Emotes + for (const auto &emote : *channel->ffzEmotes()) { + this->addString(emote.second->name.string, + TaggedString::Type::FFZChannelEmote); + } } - // Global: FFZ Global Emotes - std::vector &ffzGlobalEmoteCodes = app->emotes->ffz.globalEmoteCodes; - for (const auto &m : ffzGlobalEmoteCodes) { - this->addString(m, TaggedString::Type::FFZGlobalEmote); - } - - // Channel-specific: BTTV Channel Emotes - std::vector &bttvChannelEmoteCodes = - app->emotes->bttv.channelEmoteCodes[this->channelName_]; - for (const auto &m : bttvChannelEmoteCodes) { - this->addString(m, TaggedString::Type::BTTVChannelEmote); - } - - // Channel-specific: FFZ Channel Emotes - std::vector &ffzChannelEmoteCodes = - app->emotes->ffz.channelEmoteCodes[this->channelName_]; - for (const auto &m : ffzChannelEmoteCodes) { - this->addString(m, TaggedString::Type::FFZChannelEmote); - } - - // Global: Emojis + // Emojis const auto &emojiShortCodes = app->emotes->emojis.shortCodes; for (const auto &m : emojiShortCodes) { this->addString(":" + m + ":", TaggedString::Type::Emoji); @@ -158,7 +171,8 @@ void CompletionModel::refresh() // Channel-specific: Usernames // fourtf: only works with twitch chat - // auto c = ChannelManager::getInstance().getTwitchChannel(this->channelName); + // auto c = + // ChannelManager::getInstance().getTwitchChannel(this->channelName); // auto usernames = c->getUsernamesForCompletions(); // for (const auto &name : usernames) { // assert(!name.displayName.isEmpty()); @@ -185,9 +199,11 @@ void CompletionModel::addUser(const QString &username) auto add = [this](const QString &str) { auto ts = this->createUser(str + " "); // Always add a space at the end of completions - std::pair::iterator, bool> p = this->emotes_.insert(ts); + std::pair::iterator, bool> p = + this->emotes_.insert(ts); if (!p.second) { - // No inseration was made, figure out if we need to replace the username. + // No inseration was made, figure out if we need to replace the + // username. if (p.first->str > ts.str) { // Replace lowercase version of name with mixed-case version diff --git a/src/common/CompletionModel.hpp b/src/common/CompletionModel.hpp index 5bae63be0..d9baac7b3 100644 --- a/src/common/CompletionModel.hpp +++ b/src/common/CompletionModel.hpp @@ -10,6 +10,8 @@ namespace chatterino { +class TwitchChannel; + class CompletionModel : public QAbstractListModel { struct TaggedString { diff --git a/src/common/ConcurrentMap.hpp b/src/common/ConcurrentMap.hpp index 2db2d9f58..0b5cbeb0e 100644 --- a/src/common/ConcurrentMap.hpp +++ b/src/common/ConcurrentMap.hpp @@ -65,7 +65,8 @@ public: this->data.insert(name, value); } - void each(std::function func) const + void each( + std::function func) const { QMutexLocker lock(&this->mutex); diff --git a/src/common/Emotemap.cpp b/src/common/Emotemap.cpp deleted file mode 100644 index b8903afd9..000000000 --- a/src/common/Emotemap.cpp +++ /dev/null @@ -1,46 +0,0 @@ -#include "Emotemap.hpp" - -#include "Application.hpp" -#include "singletons/Settings.hpp" - -namespace chatterino { - -EmoteData::EmoteData(Image *image) - : image1x(image) -{ -} - -// Emotes must have a 1x image to be valid -bool EmoteData::isValid() const -{ - return this->image1x != nullptr; -} - -Image *EmoteData::getImage(float scale) const -{ - int quality = getApp()->settings->preferredEmoteQuality; - - if (quality == 0) { - scale *= getApp()->settings->emoteScale.getValue(); - quality = [&] { - if (scale <= 1) - return 1; - if (scale <= 2) - return 2; - return 3; - }(); - } - - Image *_image; - if (quality == 3 && this->image3x != nullptr) { - _image = this->image3x; - } else if (quality >= 2 && this->image2x != nullptr) { - _image = this->image2x; - } else { - _image = this->image1x; - } - - return _image; -} - -} // namespace chatterino diff --git a/src/common/Emotemap.hpp b/src/common/Emotemap.hpp deleted file mode 100644 index d4010048d..000000000 --- a/src/common/Emotemap.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "messages/Image.hpp" -#include "util/ConcurrentMap.hpp" - -namespace chatterino { - -struct EmoteData { - EmoteData() = default; - - EmoteData(Image *image); - - // Emotes must have a 1x image to be valid - bool isValid() const; - Image *getImage(float scale) const; - - // Link to the emote page i.e. https://www.frankerfacez.com/emoticon/144722-pajaCringe - QString pageLink; - - Image *image1x = nullptr; - Image *image2x = nullptr; - Image *image3x = nullptr; -}; - -using EmoteMap = ConcurrentMap; - -} // namespace chatterino diff --git a/src/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp index b1113a8c5..c5be885e5 100644 --- a/src/common/FlagsEnum.hpp +++ b/src/common/FlagsEnum.hpp @@ -4,62 +4,78 @@ namespace chatterino { -// = std::enable_if::value>::type - template ::type> class FlagsEnum { public: FlagsEnum() - : value(static_cast(0)) + : value_(static_cast(0)) { } - FlagsEnum(T _value) - : value(_value) + FlagsEnum(T value) + : value_(value) { } - inline T operator~() const + FlagsEnum(std::initializer_list flags) { - return (T) ~(Q)this->value; - } - inline T operator|(Q a) const - { - return (T)((Q)a | (Q)this->value); - } - inline T operator&(Q a) const - { - return (T)((Q)a & (Q)this->value); - } - inline T operator^(Q a) const - { - return (T)((Q)a ^ (Q)this->value); - } - inline T &operator|=(const Q &a) - { - return (T &)((Q &)this->value |= (Q)a); - } - inline T &operator&=(const Q &a) - { - return (T &)((Q &)this->value &= (Q)a); - } - inline T &operator^=(const Q &a) - { - return (T &)((Q &)this->value ^= (Q)a); + for (auto flag : flags) { + this->set(flag); + } } - void EnableFlag(T flag) + bool operator==(const FlagsEnum &other) { - reinterpret_cast(this->value) |= static_cast(flag); + return this->value_ == other.value_; } - bool HasFlag(Q flag) const + bool operator!=(const FlagsEnum &other) { - return (this->value & flag) == flag; + return this->value_ != other.value_; } - T value; + void set(T flag) + { + reinterpret_cast(this->value_) |= static_cast(flag); + } + + void unset(T flag) + { + reinterpret_cast(this->value_) &= ~static_cast(flag); + } + + void set(T flag, bool value) + { + if (value) + this->set(flag); + else + this->unset(flag); + } + + bool has(T flag) const + { + return static_cast(this->value_) & static_cast(flag); + } + + bool hasAny(FlagsEnum flags) const + { + return static_cast(this->value_) & static_cast(flags.value_); + } + + bool hasAll(FlagsEnum flags) const + { + return (static_cast(this->value_) & static_cast(flags.value_)) && + static_cast(flags->value); + } + + bool hasNone(std::initializer_list flags) const + { + return !this->hasAny(flags); + } + +private: + T value_{}; }; } // namespace chatterino diff --git a/src/common/LinkParser.cpp b/src/common/LinkParser.cpp index 5580a69c0..a80c75641 100644 --- a/src/common/LinkParser.cpp +++ b/src/common/LinkParser.cpp @@ -5,58 +5,63 @@ #include #include +// ip 0.0.0.0 - 224.0.0.0 +#define IP \ + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" \ + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" \ + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +#define PORT "(?::\\d{2,5})" +#define WEB_CHAR1 "[_a-z\\x{00a1}-\\x{ffff}0-9]" +#define WEB_CHAR2 "[a-z\\x{00a1}-\\x{ffff}0-9]" + +#define SPOTIFY_1 "(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+" +#define SPOTIFY_2 "user:[^:]+" +#define SPOTIFY_3 "search:(?:[-\\w$\\.+!*'(),]+|%[a-fA-F0-9]{2})+" +#define SPOTIFY_PARAMS "(?:" SPOTIFY_1 "|" SPOTIFY_2 "|" SPOTIFY_3 ")" +#define SPOTIFY_LINK "(?x-mi:(spotify:" SPOTIFY_PARAMS "))" + +#define WEB_PROTOCOL "(?:(?:https?|ftps?)://)?" +#define WEB_USER "(?:\\S+(?::\\S*)?@)?" +#define WEB_HOST "(?:(?:" WEB_CHAR1 "-*)*" WEB_CHAR2 "+)" +#define WEB_DOMAIN "(?:\\.(?:" WEB_CHAR2 "-*)*" WEB_CHAR2 "+)*" +#define WEB_TLD "(?:" + tldData + ")" +#define WEB_RESOURCE_PATH "(?:[/?#]\\S*)" +#define WEB_LINK \ + WEB_PROTOCOL WEB_USER "(?:" IP "|" WEB_HOST WEB_DOMAIN "\\." WEB_TLD PORT \ + "?" WEB_RESOURCE_PATH "?)" + +#define LINK "^(?:" SPOTIFY_LINK "|" WEB_LINK ")$" + namespace chatterino { LinkParser::LinkParser(const QString &unparsedString) { static QRegularExpression linkRegex = [] { static QRegularExpression newLineRegex("\r?\n"); - QFile tldFile(":/tlds.txt"); - tldFile.open(QFile::ReadOnly); + QFile file(":/tlds.txt"); + file.open(QFile::ReadOnly); + QTextStream tlds(&file); + tlds.setCodec("UTF-8"); - QTextStream t1(&tldFile); - t1.setCodec("UTF-8"); + // tldData gets injected into the LINK macro + auto tldData = tlds.readAll().replace(newLineRegex, "|"); + (void)tldData; - // Read the TLDs in and replace the newlines with pipes - QString tldData = t1.readAll().replace(newLineRegex, "|"); - - const QString urlRegExp = - "^" - // protocol identifier - "(?:(?:https?|ftps?)://)?" - // user:pass authentication - "(?:\\S+(?::\\S*)?@)?" - "(?:" - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broacast addresses - // (first & last IP address of each class) - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" - "|" - // host name - "(?:(?:[_a-z\\x{00a1}-\\x{ffff}0-9]-*)*[a-z\\x{00a1}-\\x{ffff}0-9]+)" - // domain name - "(?:\\.(?:[a-z\\x{00a1}-\\x{ffff}0-9]-*)*[a-z\\x{00a1}-\\x{ffff}0-9]+)*" - // TLD identifier - //"(?:\\.(?:[a-z\\x{00a1}-\\x{ffff}]{2,}))" - "(?:[\\.](?:" + - tldData + - "))" - "\\.?" - ")" - // port number - "(?::\\d{2,5})?" - // resource path - "(?:[/?#]\\S*)?" - "$"; - - return QRegularExpression(urlRegExp, QRegularExpression::CaseInsensitiveOption); + return QRegularExpression(LINK, + QRegularExpression::CaseInsensitiveOption); }(); this->match_ = linkRegex.match(unparsedString); } +bool LinkParser::hasMatch() const +{ + return this->match_.hasMatch(); +} + +QString LinkParser::getCaptured() const +{ + return this->match_.captured(); +} + } // namespace chatterino diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 3a1ad6fab..2d56cbde0 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -10,15 +10,8 @@ class LinkParser public: explicit LinkParser(const QString &unparsedString); - bool hasMatch() const - { - return this->match_.hasMatch(); - } - - QString getCaptured() const - { - return this->match_.captured(); - } + bool hasMatch() const; + QString getCaptured() const; private: QRegularExpressionMatch match_; diff --git a/src/common/LockedObject.hpp b/src/common/LockedObject.hpp deleted file mode 100644 index 01ffe3b66..000000000 --- a/src/common/LockedObject.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include - -namespace chatterino { - -template -class LockedObject -{ -public: - LockedObject &operator=(const LockedObject &other) - { - this->mutex_.lock(); - - this->data = other.getValue(); - - this->mutex_.unlock(); - - return *this; - } - - LockedObject &operator=(const Type &other) - { - this->mutex_.lock(); - - this->data = other; - - this->mutex_.unlock(); - - return *this; - } - -private: - Type value_; - std::mutex mutex_; -}; - -} // namespace chatterino diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp index 743eeee04..30d1e9267 100644 --- a/src/common/NetworkCommon.hpp +++ b/src/common/NetworkCommon.hpp @@ -2,13 +2,15 @@ #include +#include "Common.hpp" + class QNetworkReply; namespace chatterino { class NetworkResult; -using NetworkSuccessCallback = std::function; +using NetworkSuccessCallback = std::function; using NetworkErrorCallback = std::function; using NetworkReplyCreatedCallback = std::function; diff --git a/src/common/NetworkData.cpp b/src/common/NetworkData.cpp index 878bda409..ad8fe40b2 100644 --- a/src/common/NetworkData.cpp +++ b/src/common/NetworkData.cpp @@ -2,12 +2,23 @@ #include "Application.hpp" #include "singletons/Paths.hpp" +#include "util/DebugCount.hpp" #include #include namespace chatterino { +NetworkData::NetworkData() +{ + DebugCount::increase("NetworkData"); +} + +NetworkData::~NetworkData() +{ + DebugCount::decrease("NetworkData"); +} + QString NetworkData::getHash() { if (this->hash_.isEmpty()) { @@ -19,7 +30,8 @@ QString NetworkData::getHash() bytes.append(header); } - QByteArray hashBytes(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); + QByteArray hashBytes( + QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); this->hash_ = hashBytes.toHex(); } diff --git a/src/common/NetworkData.hpp b/src/common/NetworkData.hpp index d3cecc046..2d149fc09 100644 --- a/src/common/NetworkData.hpp +++ b/src/common/NetworkData.hpp @@ -13,9 +13,13 @@ namespace chatterino { class NetworkResult; struct NetworkData { + NetworkData(); + ~NetworkData(); + QNetworkRequest request_; const QObject *caller_ = nullptr; bool useQuickLoadCache_{}; + bool executeConcurrently{}; NetworkReplyCreatedCallback onReplyCreated_; NetworkErrorCallback onError_; diff --git a/src/common/NetworkManager.cpp b/src/common/NetworkManager.cpp index 5577afda5..1ff1bf635 100644 --- a/src/common/NetworkManager.cpp +++ b/src/common/NetworkManager.cpp @@ -5,11 +5,11 @@ namespace chatterino { QThread NetworkManager::workerThread; -QNetworkAccessManager NetworkManager::NaM; +QNetworkAccessManager NetworkManager::accessManager; void NetworkManager::init() { - NetworkManager::NaM.moveToThread(&NetworkManager::workerThread); + NetworkManager::accessManager.moveToThread(&NetworkManager::workerThread); NetworkManager::workerThread.start(); } diff --git a/src/common/NetworkManager.hpp b/src/common/NetworkManager.hpp index 0e59c0540..12c0c6a51 100644 --- a/src/common/NetworkManager.hpp +++ b/src/common/NetworkManager.hpp @@ -20,7 +20,7 @@ class NetworkManager : public QObject public: static QThread workerThread; - static QNetworkAccessManager NaM; + static QNetworkAccessManager accessManager; static void init(); static void deinit(); diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index 0311c09e9..f54192190 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -2,16 +2,19 @@ #include "Application.hpp" #include "common/NetworkManager.hpp" +#include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "singletons/Paths.hpp" +#include "util/DebugCount.hpp" #include - +#include #include namespace chatterino { -NetworkRequest::NetworkRequest(const std::string &url, NetworkRequestType requestType) +NetworkRequest::NetworkRequest(const std::string &url, + NetworkRequestType requestType) : data(new NetworkData) , timer(new NetworkTimer) { @@ -62,7 +65,8 @@ void NetworkRequest::setRawHeader(const char *headerName, const char *value) this->data->request_.setRawHeader(headerName, value); } -void NetworkRequest::setRawHeader(const char *headerName, const QByteArray &value) +void NetworkRequest::setRawHeader(const char *headerName, + const QByteArray &value) { this->data->request_.setRawHeader(headerName, value); } @@ -77,7 +81,13 @@ void NetworkRequest::setTimeout(int ms) this->timer->timeoutMS_ = ms; } -void NetworkRequest::makeAuthorizedV5(const QString &clientID, const QString &oauthToken) +void NetworkRequest::setExecuteConcurrently(bool value) +{ + this->data->executeConcurrently = value; +} + +void NetworkRequest::makeAuthorizedV5(const QString &clientID, + const QString &oauthToken) { this->setRawHeader("Client-ID", clientID); this->setRawHeader("Accept", "application/vnd.twitchtv.v5+json"); @@ -114,22 +124,24 @@ void NetworkRequest::execute() } break; case NetworkRequestType::Put: { - // Put requests cannot be cached, therefore the request is called immediately + // Put requests cannot be cached, therefore the request is called + // immediately this->doRequest(); } break; case NetworkRequestType::Delete: { - // Delete requests cannot be cached, therefore the request is called immediately + // Delete requests cannot be cached, therefore the request is called + // immediately this->doRequest(); } break; default: { - Log("[Execute] Unhandled request type"); + log("[Execute] Unhandled request type"); } break; } } -bool NetworkRequest::tryLoadCachedFile() +Outcome NetworkRequest::tryLoadCachedFile() { auto app = getApp(); @@ -137,28 +149,31 @@ bool NetworkRequest::tryLoadCachedFile() if (!cachedFile.exists()) { // File didn't exist - return false; + return Failure; } if (!cachedFile.open(QIODevice::ReadOnly)) { // File could not be opened - return false; + return Failure; } QByteArray bytes = cachedFile.readAll(); NetworkResult result(bytes); - bool success = this->data->onSuccess_(result); + auto outcome = this->data->onSuccess_(result); cachedFile.close(); - // XXX: If success is false, we should invalidate the cache file somehow/somewhere + // XXX: If success is false, we should invalidate the cache file + // somehow/somewhere - return success; + return outcome; } void NetworkRequest::doRequest() { + DebugCount::increase("http request started"); + NetworkRequester requester; NetworkWorker *worker = new NetworkWorker; @@ -166,30 +181,34 @@ void NetworkRequest::doRequest() this->timer->start(); - auto onUrlRequested = [data = this->data, timer = this->timer, worker]() mutable { - QNetworkReply *reply = nullptr; - switch (data->requestType_) { - case NetworkRequestType::Get: { - reply = NetworkManager::NaM.get(data->request_); - } break; + auto onUrlRequested = [data = this->data, timer = this->timer, + worker]() mutable { + auto reply = [&]() -> QNetworkReply * { + switch (data->requestType_) { + case NetworkRequestType::Get: + return NetworkManager::accessManager.get(data->request_); - case NetworkRequestType::Put: { - reply = NetworkManager::NaM.put(data->request_, data->payload_); - } break; + case NetworkRequestType::Put: + return NetworkManager::accessManager.put(data->request_, + data->payload_); - case NetworkRequestType::Delete: { - reply = NetworkManager::NaM.deleteResource(data->request_); - } break; - } + case NetworkRequestType::Delete: + return NetworkManager::accessManager.deleteResource( + data->request_); + + default: + return nullptr; + } + }(); if (reply == nullptr) { - Log("Unhandled request type"); + log("Unhandled request type"); return; } if (timer->isStarted()) { timer->onTimeout(worker, [reply, data]() { - Log("Aborted!"); + log("Aborted!"); reply->abort(); if (data->onError_) { data->onError_(-2); @@ -201,8 +220,6 @@ void NetworkRequest::doRequest() data->onReplyCreated_(reply); } - bool directAction = (data->caller_ == nullptr); - auto handleReply = [data, timer, reply]() mutable { // TODO(pajlada): A reply was received, kill the timeout timer if (reply->error() != QNetworkReply::NetworkError::NoError) { @@ -212,28 +229,38 @@ void NetworkRequest::doRequest() return; } - QByteArray readBytes = reply->readAll(); - QByteArray bytes; - bytes.setRawData(readBytes.data(), readBytes.size()); + QByteArray bytes = reply->readAll(); data->writeToCache(bytes); NetworkResult result(bytes); - data->onSuccess_(result); + + DebugCount::increase("http request success"); + // log("starting {}", data->request_.url().toString()); + if (data->onSuccess_) { + if (data->executeConcurrently) + QtConcurrent::run( + [onSuccess = std::move(data->onSuccess_), + result = std::move(result)] { onSuccess(result); }); + else + data->onSuccess_(result); + } + // log("finished {}", data->request_.url().toString()); reply->deleteLater(); }; if (data->caller_ != nullptr) { QObject::connect(worker, &NetworkWorker::doneUrl, data->caller_, - std::move(handleReply)); - QObject::connect(reply, &QNetworkReply::finished, worker, [worker]() mutable { - emit worker->doneUrl(); + handleReply); + QObject::connect(reply, &QNetworkReply::finished, worker, + [worker]() mutable { + emit worker->doneUrl(); - delete worker; - }); + delete worker; + }); } else { QObject::connect(reply, &QNetworkReply::finished, worker, - [handleReply = std::move(handleReply), worker]() mutable { + [handleReply, worker]() mutable { handleReply(); delete worker; @@ -241,7 +268,8 @@ void NetworkRequest::doRequest() } }; - QObject::connect(&requester, &NetworkRequester::requestUrl, worker, onUrlRequested); + QObject::connect(&requester, &NetworkRequester::requestUrl, worker, + onUrlRequested); emit requester.requestUrl(); } diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp index e2fd2e7e4..09b2cccee 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/NetworkRequest.hpp @@ -12,29 +12,30 @@ namespace chatterino { class NetworkRequest { - // Stores all data about the request that needs to be passed around to each part of the request + // Stores all data about the request that needs to be passed around to each + // part of the request std::shared_ptr data; // Timer that tracks the timeout // By default, there's no explicit timeout for the request - // to enable the timer, the "setTimeout" function needs to be called before execute is called + // to enable the timer, the "setTimeout" function needs to be called before + // execute is called std::shared_ptr timer; - // The NetworkRequest destructor will assert if executed_ hasn't been set to true before dying + // The NetworkRequest destructor will assert if executed_ hasn't been set to + // true before dying bool executed_ = false; public: - NetworkRequest() = delete; - NetworkRequest(const NetworkRequest &other) = delete; - NetworkRequest &operator=(const NetworkRequest &other) = delete; + explicit NetworkRequest( + const std::string &url, + NetworkRequestType requestType = NetworkRequestType::Get); + explicit NetworkRequest( + QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); NetworkRequest(NetworkRequest &&other) = default; NetworkRequest &operator=(NetworkRequest &&other) = default; - explicit NetworkRequest(const std::string &url, - NetworkRequestType requestType = NetworkRequestType::Get); - NetworkRequest(QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); - ~NetworkRequest(); void setRequestType(NetworkRequestType newRequestType); @@ -50,15 +51,15 @@ public: void setRawHeader(const char *headerName, const QByteArray &value); void setRawHeader(const char *headerName, const QString &value); void setTimeout(int ms); - void makeAuthorizedV5(const QString &clientID, const QString &oauthToken = QString()); + void setExecuteConcurrently(bool value); + void makeAuthorizedV5(const QString &clientID, + const QString &oauthToken = QString()); void execute(); private: - // Returns true if the file was successfully loaded from cache - // Returns false if the cache file either didn't exist, or it contained "invalid" data - // "invalid" is specified by the onSuccess callback - bool tryLoadCachedFile(); + // "invalid" data "invalid" is specified by the onSuccess callback + Outcome tryLoadCachedFile(); void doRequest(); diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp index 80eb582ff..78884a421 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/NetworkResult.cpp @@ -25,20 +25,21 @@ QJsonObject NetworkResult::parseJson() const rapidjson::Document NetworkResult::parseRapidJson() const { - rapidjson::Document ret(rapidjson::kNullType); + rapidjson::Document ret(rapidjson::kObjectType); - rapidjson::ParseResult result = ret.Parse(this->data_.data(), this->data_.length()); + rapidjson::ParseResult result = + ret.Parse(this->data_.data(), this->data_.length()); if (result.Code() != rapidjson::kParseErrorNone) { - Log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), - result.Offset()); + log("JSON parse error: {} ({})", + rapidjson::GetParseError_En(result.Code()), result.Offset()); return ret; } return ret; } -QByteArray NetworkResult::getData() const +const QByteArray &NetworkResult::getData() const { return this->data_; } diff --git a/src/common/NetworkResult.hpp b/src/common/NetworkResult.hpp index 5631e1fe4..36a23de22 100644 --- a/src/common/NetworkResult.hpp +++ b/src/common/NetworkResult.hpp @@ -7,14 +7,15 @@ namespace chatterino { class NetworkResult { - QByteArray data_; - public: NetworkResult(const QByteArray &data); QJsonObject parseJson() const; rapidjson::Document parseRapidJson() const; - QByteArray getData() const; + const QByteArray &getData() const; + +private: + QByteArray data_; }; } // namespace chatterino diff --git a/src/common/NetworkTimer.cpp b/src/common/NetworkTimer.cpp index 522fdd310..858378b17 100644 --- a/src/common/NetworkTimer.cpp +++ b/src/common/NetworkTimer.cpp @@ -28,7 +28,8 @@ bool NetworkTimer::isStarted() const return this->started_; } -void NetworkTimer::onTimeout(NetworkWorker *worker, std::function cb) const +void NetworkTimer::onTimeout(NetworkWorker *worker, + std::function cb) const { assert(this->timer_ != nullptr); assert(worker != nullptr); diff --git a/src/common/NullablePtr.hpp b/src/common/NullablePtr.hpp index f38ecfa5d..8bc160fd8 100644 --- a/src/common/NullablePtr.hpp +++ b/src/common/NullablePtr.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace chatterino { template @@ -23,7 +25,7 @@ public: return element_; } - T &operator*() const + typename std::add_lvalue_reference::type operator*() const { assert(this->hasElement()); @@ -52,6 +54,18 @@ public: return this->hasElement(); } + bool operator!() const + { + return !this->hasElement(); + } + + template ::value>> + operator NullablePtr() const + { + return NullablePtr(this->element_); + } + private: T *element_; }; diff --git a/src/common/Outcome.hpp b/src/common/Outcome.hpp new file mode 100644 index 000000000..01be69fd8 --- /dev/null +++ b/src/common/Outcome.hpp @@ -0,0 +1,51 @@ +#pragma once + +namespace chatterino { + +struct SuccessTag { +}; + +struct FailureTag { +}; + +const SuccessTag Success{}; +const FailureTag Failure{}; + +class Outcome +{ +public: + Outcome(SuccessTag) + : success_(true) + { + } + + Outcome(FailureTag) + : success_(false) + { + } + + explicit operator bool() const + { + return this->success_; + } + + bool operator!() const + { + return !this->success_; + } + + bool operator==(const Outcome &other) const + { + return this->success_ == other.success_; + } + + bool operator!=(const Outcome &other) const + { + return !this->operator==(other); + } + +private: + bool success_; +}; + +} // namespace chatterino diff --git a/src/common/Property.hpp b/src/common/Property.hpp deleted file mode 100644 index 99d0efa48..000000000 --- a/src/common/Property.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "boost/noncopyable.hpp" - -namespace chatterino { - -template -class Property final : boost::noncopyable -{ -public: - Property() - { - } - - Property(const T &value) - : value_(value) - { - } - - T &operator=(const T &f) - { - return value_ = f; - } - - operator T const &() const - { - return value_; - } - -protected: - T value_; -}; - -} // namespace chatterino diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp index 0594b8847..1ddf2c13a 100644 --- a/src/common/SignalVector.hpp +++ b/src/common/SignalVector.hpp @@ -88,7 +88,8 @@ template class UnsortedSignalVector : public BaseSignalVector { public: - virtual int insertItem(const TVectorItem &item, int index = -1, void *caller = nullptr) override + virtual int insertItem(const TVectorItem &item, int index = -1, + void *caller = nullptr) override { assertInGuiThread(); if (index == -1) { @@ -115,11 +116,13 @@ template class SortedSignalVector : public BaseSignalVector { public: - virtual int insertItem(const TVectorItem &item, int = -1, void *caller = nullptr) override + virtual int insertItem(const TVectorItem &item, int = -1, + void *caller = nullptr) override { assertInGuiThread(); - auto it = std::lower_bound(this->vector_.begin(), this->vector_.end(), item, Compare{}); + auto it = std::lower_bound(this->vector_.begin(), this->vector_.end(), + item, Compare{}); int index = it - this->vector_.begin(); this->vector_.insert(it, item); diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index cab31c063..237727d0d 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -11,7 +11,8 @@ namespace chatterino { template -class SignalVectorModel : public QAbstractTableModel, pajlada::Signals::SignalHolder +class SignalVectorModel : public QAbstractTableModel, + pajlada::Signals::SignalHolder { public: SignalVectorModel(int columnCount, QObject *parent = nullptr) @@ -43,7 +44,8 @@ public: index = this->beforeInsert(args.item, row, index); this->beginInsertRows(QModelIndex(), index, index); - this->rows_.insert(this->rows_.begin() + index, Row(row, args.item)); + this->rows_.insert(this->rows_.begin() + index, + Row(row, args.item)); this->endInsertRows(); }; @@ -65,7 +67,8 @@ public: assert(row >= 0 && row <= this->rows_.size()); // remove row - std::vector items = std::move(this->rows_[row].items); + std::vector items = + std::move(this->rows_[row].items); this->beginRemoveRows(QModelIndex(), row, row); this->rows_.erase(this->rows_.begin() + row); @@ -103,15 +106,18 @@ public: QVariant data(const QModelIndex &index, int role) const override { int row = index.row(), column = index.column(); - assert(row >= 0 && row < this->rows_.size() && column >= 0 && column < this->columnCount_); + assert(row >= 0 && row < this->rows_.size() && column >= 0 && + column < this->columnCount_); return rows_[row].items[column]->data(role); } - bool setData(const QModelIndex &index, const QVariant &value, int role) override + bool setData(const QModelIndex &index, const QVariant &value, + int role) override { int row = index.row(), column = index.column(); - assert(row >= 0 && row < this->rows_.size() && column >= 0 && column < this->columnCount_); + assert(row >= 0 && row < this->rows_.size() && column >= 0 && + column < this->columnCount_); Row &rowItem = this->rows_[row]; @@ -124,15 +130,16 @@ public: this->vector_->removeItem(vecRow, this); assert(this->rows_[row].original); - TVectorItem item = - this->getItemFromRow(this->rows_[row].items, this->rows_[row].original.get()); + TVectorItem item = this->getItemFromRow( + this->rows_[row].items, this->rows_[row].original.get()); this->vector_->insertItem(item, vecRow, this); } return true; } - QVariant headerData(int section, Qt::Orientation orientation, int role) const override + QVariant headerData(int section, Qt::Orientation orientation, + int role) const override { if (orientation != Qt::Horizontal) { return QVariant(); @@ -146,7 +153,8 @@ public: } } - bool setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, + bool setHeaderData(int section, Qt::Orientation orientation, + const QVariant &value, int role = Qt::DisplayRole) override { if (orientation != Qt::Horizontal) { @@ -162,14 +170,16 @@ public: Qt::ItemFlags flags(const QModelIndex &index) const override { int row = index.row(), column = index.column(); - assert(row >= 0 && row < this->rows_.size() && column >= 0 && column < this->columnCount_); + assert(row >= 0 && row < this->rows_.size() && column >= 0 && + column < this->columnCount_); return this->rows_[index.row()].items[index.column()]->flags(); } QStandardItem *getItem(int row, int column) { - assert(row >= 0 && row < this->rows_.size() && column >= 0 && column < this->columnCount_); + assert(row >= 0 && row < this->rows_.size() && column >= 0 && + column < this->columnCount_); return rows_[row].items[column]; } @@ -204,20 +214,23 @@ protected: const TVectorItem &original) = 0; // turns a row in the model into a vector item - virtual void getRowFromItem(const TVectorItem &item, std::vector &row) = 0; + virtual void getRowFromItem(const TVectorItem &item, + std::vector &row) = 0; - virtual int beforeInsert(const TVectorItem &item, std::vector &row, + virtual int beforeInsert(const TVectorItem &item, + std::vector &row, int proposedIndex) { return proposedIndex; } - virtual void afterRemoved(const TVectorItem &item, std::vector &row, int index) + virtual void afterRemoved(const TVectorItem &item, + std::vector &row, int index) { } - virtual void customRowSetData(const std::vector &row, int column, - const QVariant &value, int role) + virtual void customRowSetData(const std::vector &row, + int column, const QVariant &value, int role) { } @@ -226,7 +239,8 @@ protected: assert(index >= 0 && index <= this->rows_.size()); this->beginInsertRows(QModelIndex(), index, index); - this->rows_.insert(this->rows_.begin() + index, Row(std::move(row), true)); + this->rows_.insert(this->rows_.begin() + index, + Row(std::move(row), true)); this->endInsertRows(); } diff --git a/src/common/SimpleSignalVector.hpp b/src/common/SimpleSignalVector.hpp deleted file mode 100644 index 6aca458f5..000000000 --- a/src/common/SimpleSignalVector.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace chatterino { - -template -class SimpleSignalVector -{ -public: - SimpleSignalVector &operator=(std::vector &other) - { - this->data_ = other; - - this->updated.invoke(); - - return *this; - } - - operator std::vector &() - { - return this->data_; - } - - pajlada::Signals::NoArgSignal updated; - -private: - std::vector data_; -}; - -} // namespace chatterino diff --git a/src/common/Singleton.hpp b/src/common/Singleton.hpp index 474c31f0f..a98dedac2 100644 --- a/src/common/Singleton.hpp +++ b/src/common/Singleton.hpp @@ -4,14 +4,18 @@ namespace chatterino { -class Application; +class Settings; +class Paths; class Singleton : boost::noncopyable { public: - virtual void initialize(Application &app) + virtual ~Singleton() = default; + + virtual void initialize(Settings &settings, Paths &paths) { - (void)(app); + (void)(settings); + (void)(paths); } virtual void save() diff --git a/src/common/UniqueAccess.hpp b/src/common/UniqueAccess.hpp new file mode 100644 index 000000000..30d6bee32 --- /dev/null +++ b/src/common/UniqueAccess.hpp @@ -0,0 +1,102 @@ +#pragma once + +#include +#include + +namespace chatterino { + +template +class AccessGuard +{ +public: + AccessGuard(T &element, std::mutex &mutex) + : element_(&element) + , mutex_(&mutex) + { + this->mutex_->lock(); + } + + AccessGuard(AccessGuard &&other) + : element_(other.element_) + , mutex_(other.mutex_) + { + other.isValid_ = false; + } + + AccessGuard &operator=(AccessGuard &&other) + { + other.isValid_ = false; + this->element_ = other.element_; + this->mutex_ = other.element_; + } + + ~AccessGuard() + { + if (this->isValid_) this->mutex_->unlock(); + } + + T *operator->() const + { + return this->element_; + } + + T &operator*() const + { + return *this->element_; + } + +private: + T *element_{}; + std::mutex *mutex_{}; + bool isValid_{true}; +}; + +template +class UniqueAccess +{ +public: + UniqueAccess() + : element_(T()) + { + } + + UniqueAccess(const T &element) + : element_(element) + { + } + + UniqueAccess(T &&element) + : element_(element) + { + } + + UniqueAccess &operator=(const T &element) + { + this->element_ = element; + return *this; + } + + UniqueAccess &operator=(T &&element) + { + this->element_ = element; + return *this; + } + + AccessGuard access() const + { + return AccessGuard(this->element_, this->mutex_); + } + + template ::value>> + AccessGuard accessConst() const + { + return AccessGuard(this->element_, this->mutex_); + } + +private: + mutable T element_; + mutable std::mutex mutex_; +}; + +} // namespace chatterino diff --git a/src/common/UrlFetch.hpp b/src/common/UrlFetch.hpp deleted file mode 100644 index 5e27fd3d7..000000000 --- a/src/common/UrlFetch.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "common/NetworkRequest.hpp" - -#include -#include - -namespace chatterino { - -// Not sure if I like these, but I'm trying them out - -static NetworkRequest makeGetChannelRequest(const QString &channelId, - const QObject *caller = nullptr) -{ - QString url("https://api.twitch.tv/kraken/channels/" + channelId); - - auto request = NetworkRequest::twitchRequest(url); - - request.setCaller(caller); - - return request; -} - -static NetworkRequest makeGetStreamRequest(const QString &channelId, - const QObject *caller = nullptr) -{ - QString url("https://api.twitch.tv/kraken/streams/" + channelId); - - auto request = NetworkRequest::twitchRequest(url); - - request.setCaller(caller); - - return request; -} - -} // namespace chatterino diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 0341a06a6..6cd1f2558 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -7,7 +7,8 @@ namespace chatterino { AccountController::AccountController() { this->twitch.accounts.itemInserted.connect([this](const auto &args) { - this->accounts_.insertItem(std::dynamic_pointer_cast(args.item)); + this->accounts_.insertItem( + std::dynamic_pointer_cast(args.item)); }); this->twitch.accounts.itemRemoved.connect([this](const auto &args) { @@ -33,7 +34,7 @@ AccountController::AccountController() }); } -void AccountController::initialize(Application &app) +void AccountController::initialize(Settings &settings, Paths &paths) { this->twitch.load(); } diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp index 69fc7f092..023ad9c9e 100644 --- a/src/controllers/accounts/AccountController.hpp +++ b/src/controllers/accounts/AccountController.hpp @@ -11,21 +11,25 @@ namespace chatterino { +class Settings; +class Paths; + class AccountModel; -class AccountController : public Singleton +class AccountController final : public Singleton { public: AccountController(); AccountModel *createModel(QObject *parent); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; TwitchAccountManager twitch; private: - SortedSignalVector, SharedPtrElementLess> accounts_; + SortedSignalVector, SharedPtrElementLess> + accounts_; }; } // namespace chatterino diff --git a/src/controllers/accounts/AccountModel.cpp b/src/controllers/accounts/AccountModel.cpp index d18946468..d63f2c288 100644 --- a/src/controllers/accounts/AccountModel.cpp +++ b/src/controllers/accounts/AccountModel.cpp @@ -10,8 +10,8 @@ AccountModel::AccountModel(QObject *parent) } // turn a vector item into a model row -std::shared_ptr AccountModel::getItemFromRow(std::vector &, - const std::shared_ptr &original) +std::shared_ptr AccountModel::getItemFromRow( + std::vector &, const std::shared_ptr &original) { return original; } @@ -25,7 +25,8 @@ void AccountModel::getRowFromItem(const std::shared_ptr &item, } int AccountModel::beforeInsert(const std::shared_ptr &item, - std::vector &row, int proposedIndex) + std::vector &row, + int proposedIndex) { if (this->categoryCount_[item->getCategory()]++ == 0) { auto row = this->createRow(); diff --git a/src/controllers/accounts/AccountModel.hpp b/src/controllers/accounts/AccountModel.hpp index 0ac239a2a..c0b4538d5 100644 --- a/src/controllers/accounts/AccountModel.hpp +++ b/src/controllers/accounts/AccountModel.hpp @@ -18,17 +18,20 @@ public: protected: // turn a vector item into a model row virtual std::shared_ptr getItemFromRow( - std::vector &row, const std::shared_ptr &original) override; + std::vector &row, + const std::shared_ptr &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const std::shared_ptr &item, std::vector &row) override; virtual int beforeInsert(const std::shared_ptr &item, - std::vector &row, int proposedIndex) override; + std::vector &row, + int proposedIndex) override; virtual void afterRemoved(const std::shared_ptr &item, - std::vector &row, int index) override; + std::vector &row, + int index) override; friend class AccountController; diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 8d33391f9..3aaefca8b 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -6,22 +6,25 @@ #include "controllers/commands/Command.hpp" #include "controllers/commands/CommandModel.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/twitch/TwitchApi.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "util/CombinePath.hpp" #include "widgets/dialogs/LogsPopup.hpp" #include #include #include -#define TWITCH_DEFAULT_COMMANDS \ - { \ - "/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", "/unban", "/timeout", \ - "/untimeout", "/slow", "/slowoff", "/r9kbeta", "/r9kbetaoff", "/emoteonly", \ - "/emoteonlyoff", "/clear", "/subscribers", "/subscribersoff", "/followers", \ - "/followersoff" \ +#define TWITCH_DEFAULT_COMMANDS \ + { \ + "/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \ + "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \ + "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \ + "/clear", "/subscribers", "/subscribersoff", "/followers", \ + "/followersoff" \ } namespace chatterino { @@ -43,15 +46,14 @@ CommandController::CommandController() this->items.itemRemoved.connect(addFirstMatchToMap); } -void CommandController::initialize(Application &app) +void CommandController::initialize(Settings &, Paths &paths) { - this->load(); + this->load(paths); } -void CommandController::load() +void CommandController::load(Paths &paths) { - auto app = getApp(); - this->filePath_ = app->paths->settingsDirectory + "/commands.txt"; + this->filePath_ = combinePath(paths.settingsDirectory, "commands.txt"); QFile textFile(this->filePath_); if (!textFile.open(QIODevice::ReadOnly)) { @@ -76,7 +78,8 @@ void CommandController::save() { QFile textFile(this->filePath_); if (!textFile.open(QIODevice::WriteOnly)) { - Log("[CommandController::saveCommands] Unable to open {} for writing", this->filePath_); + log("[CommandController::saveCommands] Unable to open {} for writing", + this->filePath_); return; } @@ -95,7 +98,8 @@ CommandModel *CommandController::createModel(QObject *parent) return model; } -QString CommandController::execCommand(const QString &text, ChannelPtr channel, bool dryRun) +QString CommandController::execCommand(const QString &text, ChannelPtr channel, + bool dryRun) { QStringList words = text.split(' ', QString::SkipEmptyParts); Command command; @@ -121,11 +125,13 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, MessageBuilder b; b.emplace(); - b.emplace(app->accounts->twitch.getCurrent()->getUserName(), - MessageElement::Text, MessageColor::Text, - FontStyle::ChatMediumBold); - b.emplace("->", MessageElement::Text); - b.emplace(words[1] + ":", MessageElement::Text, MessageColor::Text, + b.emplace( + app->accounts->twitch.getCurrent()->getUserName(), + MessageElementFlag::Text, MessageColor::Text, + FontStyle::ChatMediumBold); + b.emplace("->", MessageElementFlag::Text); + b.emplace(words[1] + ":", MessageElementFlag::Text, + MessageColor::Text, FontStyle::ChatMediumBold); QString rest = ""; @@ -134,16 +140,19 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, rest += words[i] + " "; } - b.emplace(rest, MessageElement::Text); - b.getMessage()->flags |= Message::DoNotTriggerNotification; + b.emplace(rest, MessageElementFlag::Text); + b->flags.set(MessageFlag::DoNotTriggerNotification); + auto messagexD = b.release(); - app->twitch.server->whispersChannel->addMessage(b.getMessage()); + app->twitch.server->whispersChannel->addMessage(messagexD); - app->twitch.server->getWriteConnection()->sendRaw("PRIVMSG #jtv :" + text + "\r\n"); + app->twitch.server->sendMessage("jtv", text); - if (app->settings->inlineWhispers) { + if (getSettings()->inlineWhispers) { app->twitch.server->forEachChannel( - [&b](ChannelPtr _channel) { _channel->addMessage(b.getMessage()); }); + [&messagexD](ChannelPtr _channel) { + _channel->addMessage(messagexD); + }); } return ""; @@ -158,56 +167,131 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, if (commandName == "/debug-args") { QString msg = QApplication::instance()->arguments().join(' '); - channel->addMessage(Message::createSystemMessage(msg)); + channel->addMessage(makeSystemMessage(msg)); return ""; } else if (commandName == "/uptime") { - const auto &streamStatus = twitchChannel->getStreamStatus(); + const auto &streamStatus = twitchChannel->accessStreamStatus(); - QString messageText = - streamStatus.live ? streamStatus.uptime : "Channel is not live."; + QString messageText = streamStatus->live + ? streamStatus->uptime + : "Channel is not live."; - channel->addMessage(Message::createSystemMessage(messageText)); + channel->addMessage(makeSystemMessage(messageText)); return ""; - } else if (commandName == "/ignore" && words.size() >= 2) { + } else if (commandName == "/ignore") { + if (words.size() < 2) { + channel->addMessage( + makeSystemMessage("Usage: /ignore [user]")); + return ""; + } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { - channel->addMessage( - Message::createSystemMessage("You must be logged in to ignore someone")); + channel->addMessage(makeSystemMessage( + "You must be logged in to ignore someone")); return ""; } - user->ignore(target, [channel](auto resultCode, const QString &message) { - channel->addMessage(Message::createSystemMessage(message)); - }); + user->ignore( + target, [channel](auto resultCode, const QString &message) { + channel->addMessage(makeSystemMessage(message)); + }); return ""; - } else if (commandName == "/unignore" && words.size() >= 2) { + } else if (commandName == "/unignore") { + if (words.size() < 2) { + channel->addMessage( + makeSystemMessage("Usage: /unignore [user]")); + return ""; + } auto app = getApp(); auto user = app->accounts->twitch.getCurrent(); auto target = words.at(1); if (user->isAnon()) { - channel->addMessage( - Message::createSystemMessage("You must be logged in to ignore someone")); + channel->addMessage(makeSystemMessage( + "You must be logged in to ignore someone")); return ""; } - user->unignore(target, [channel](auto resultCode, const QString &message) { - channel->addMessage(Message::createSystemMessage(message)); - }); + user->unignore( + target, [channel](auto resultCode, const QString &message) { + channel->addMessage(makeSystemMessage(message)); + }); + + return ""; + } else if (commandName == "/follow") { + if (words.size() < 2) { + channel->addMessage( + makeSystemMessage("Usage: /follow [user]")); + return ""; + } + auto app = getApp(); + + auto user = app->accounts->twitch.getCurrent(); + auto target = words.at(1); + + if (user->isAnon()) { + channel->addMessage(makeSystemMessage( + "You must be logged in to follow someone")); + 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]() { + channel->addMessage(makeSystemMessage( + "You successfully followed " + target)); + }); + }); + + return ""; + } else if (commandName == "/unfollow") { + if (words.size() < 2) { + channel->addMessage( + makeSystemMessage("Usage: /unfollow [user]")); + return ""; + } + auto app = getApp(); + + auto user = app->accounts->twitch.getCurrent(); + auto target = words.at(1); + + if (user->isAnon()) { + channel->addMessage(makeSystemMessage( + "You must be logged in to follow someone")); + 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]() { + channel->addMessage(makeSystemMessage( + "You successfully unfollowed " + target)); + }); + }); return ""; } else if (commandName == "/logs") { if (words.size() < 2) { channel->addMessage( - Message::createSystemMessage("Usage: /logs [user] (channel)")); + makeSystemMessage("Usage: /logs [user] (channel)")); return ""; } auto app = getApp(); @@ -226,7 +310,8 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, if (words.at(2).at(0) == "#") { channelName = words.at(2).mid(1); } - auto logsChannel = app->twitch.server->getChannelOrEmpty(channelName); + auto logsChannel = + app->twitch.server->getChannelOrEmpty(channelName); if (logsChannel == nullptr) { } else { logs->setInfo(logsChannel, target); @@ -252,7 +337,8 @@ QString CommandController::execCommand(const QString &text, ChannelPtr channel, return this->execCustomCommand(words, command); } -QString CommandController::execCustomCommand(const QStringList &words, const Command &command) +QString CommandController::execCustomCommand(const QStringList &words, + const Command &command) { QString result; @@ -264,13 +350,15 @@ QString CommandController::execCustomCommand(const QStringList &words, const Com int matchOffset = 0; while (true) { - QRegularExpressionMatch match = parseCommand.match(command.func, matchOffset); + QRegularExpressionMatch match = + parseCommand.match(command.func, matchOffset); if (!match.hasMatch()) { break; } - result += command.func.mid(lastCaptureEnd, match.capturedStart() - lastCaptureEnd + 1); + result += command.func.mid(lastCaptureEnd, + match.capturedStart() - lastCaptureEnd + 1); lastCaptureEnd = match.capturedEnd(); matchOffset = lastCaptureEnd - 1; diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index c1cbfd855..f7b43b8f6 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -10,19 +10,23 @@ #include "controllers/commands/Command.hpp" namespace chatterino { + +class Settings; +class Paths; class Channel; class CommandModel; -class CommandController : public Singleton +class CommandController final : public Singleton { public: CommandController(); - QString execCommand(const QString &text, std::shared_ptr channel, bool dryRun); + QString execCommand(const QString &text, std::shared_ptr channel, + bool dryRun); QStringList getDefaultTwitchCommandList(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; CommandModel *createModel(QObject *parent); @@ -30,7 +34,7 @@ public: UnsortedSignalVector items; private: - void load(); + void load(Paths &paths); QMap commandsMap_; diff --git a/src/controllers/commands/CommandModel.cpp b/src/controllers/commands/CommandModel.cpp index 66e088533..e9c7cc529 100644 --- a/src/controllers/commands/CommandModel.cpp +++ b/src/controllers/commands/CommandModel.cpp @@ -9,18 +9,23 @@ CommandModel::CommandModel(QObject *parent) } // turn a vector item into a model row -Command CommandModel::getItemFromRow(std::vector &row, const Command &original) +Command CommandModel::getItemFromRow(std::vector &row, + const Command &original) { - return Command(row[0]->data(Qt::EditRole).toString(), row[1]->data(Qt::EditRole).toString()); + return Command(row[0]->data(Qt::EditRole).toString(), + row[1]->data(Qt::EditRole).toString()); } // turns a row in the model into a vector item -void CommandModel::getRowFromItem(const Command &item, 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[0]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | + Qt::ItemIsEditable); row[1]->setData(item.func, Qt::DisplayRole); - row[1]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + row[1]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | + Qt::ItemIsEditable); } } // namespace chatterino diff --git a/src/controllers/commands/CommandModel.hpp b/src/controllers/commands/CommandModel.hpp index 434b8dfd9..648dcf73a 100644 --- a/src/controllers/commands/CommandModel.hpp +++ b/src/controllers/commands/CommandModel.hpp @@ -19,7 +19,8 @@ protected: const Command &command) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const Command &item, std::vector &row) override; + virtual void getRowFromItem(const Command &item, + std::vector &row) override; friend class CommandController; }; diff --git a/src/controllers/highlights/HighlightBlacklistModel.hpp b/src/controllers/highlights/HighlightBlacklistModel.hpp index 494640833..d073425a5 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.hpp +++ b/src/controllers/highlights/HighlightBlacklistModel.hpp @@ -15,8 +15,9 @@ class HighlightBlacklistModel : public SignalVectorModel protected: // turn a vector item into a model row - virtual HighlightBlacklistUser getItemFromRow(std::vector &row, - const HighlightBlacklistUser &original) override; + virtual HighlightBlacklistUser getItemFromRow( + std::vector &row, + const HighlightBlacklistUser &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const HighlightBlacklistUser &item, diff --git a/src/controllers/highlights/HighlightBlacklistUser.hpp b/src/controllers/highlights/HighlightBlacklistUser.hpp index 122a53e89..4ce8683d0 100644 --- a/src/controllers/highlights/HighlightBlacklistUser.hpp +++ b/src/controllers/highlights/HighlightBlacklistUser.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -16,14 +16,16 @@ class HighlightBlacklistUser public: bool operator==(const HighlightBlacklistUser &other) const { - return std::tie(this->pattern_, this->isRegex_) == std::tie(other.pattern_, other.isRegex_); + return std::tie(this->pattern_, this->isRegex_) == + std::tie(other.pattern_, other.isRegex_); } HighlightBlacklistUser(const QString &pattern, bool isRegex = false) : pattern_(pattern) , isRegex_(isRegex) - , regex_(isRegex ? pattern : "", QRegularExpression::CaseInsensitiveOption | - QRegularExpression::UseUnicodePropertiesOption) + , regex_(isRegex ? pattern : "", + QRegularExpression::CaseInsensitiveOption | + QRegularExpression::UseUnicodePropertiesOption) { } diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp index 02c420c79..bd78b65ac 100644 --- a/src/controllers/highlights/HighlightController.cpp +++ b/src/controllers/highlights/HighlightController.cpp @@ -12,7 +12,7 @@ HighlightController::HighlightController() { } -void HighlightController::initialize(Application &app) +void HighlightController::initialize(Settings &settings, Paths &paths) { assert(!this->initialized_); this->initialized_ = true; @@ -24,6 +24,15 @@ void HighlightController::initialize(Application &app) 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()); + }); } HighlightModel *HighlightController::createModel(QObject *parent) @@ -54,7 +63,8 @@ bool HighlightController::isHighlightedUser(const QString &username) return false; } -HighlightBlacklistModel *HighlightController::createBlacklistModel(QObject *parent) +HighlightBlacklistModel *HighlightController::createBlacklistModel( + QObject *parent) { auto *model = new HighlightBlacklistModel(parent); model->init(&this->blacklistedUsers); @@ -64,7 +74,8 @@ HighlightBlacklistModel *HighlightController::createBlacklistModel(QObject *pare bool HighlightController::blacklistContains(const QString &username) { - std::vector blacklistItems = this->blacklistedUsers.getVector(); + std::vector blacklistItems = + this->blacklistedUsers.getVector(); for (const auto &blacklistedUser : blacklistItems) { if (blacklistedUser.isMatch(username)) { return true; diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp index a881499cf..09d686cb2 100644 --- a/src/controllers/highlights/HighlightController.hpp +++ b/src/controllers/highlights/HighlightController.hpp @@ -10,16 +10,19 @@ namespace chatterino { +class Settings; +class Paths; + class UserHighlightModel; class HighlightModel; class HighlightBlacklistModel; -class HighlightController : public Singleton +class HighlightController final : public Singleton { public: HighlightController(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector phrases; UnsortedSignalVector blacklistedUsers; @@ -39,7 +42,8 @@ private: ChatterinoSetting> highlightsSetting_ = { "/highlighting/highlights"}; - ChatterinoSetting> blacklistSetting_ = {"/highlighting/blacklist"}; + ChatterinoSetting> blacklistSetting_ = { + "/highlighting/blacklist"}; }; } // namespace chatterino diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index fcc36c3b3..cad2fd006 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -13,18 +13,20 @@ HighlightModel::HighlightModel(QObject *parent) } // turn a vector item into a model row -HighlightPhrase HighlightModel::getItemFromRow(std::vector &row, - const HighlightPhrase &original) +HighlightPhrase HighlightModel::getItemFromRow( + std::vector &row, const HighlightPhrase &original) { // key, alert, sound, regex - return HighlightPhrase{ - row[0]->data(Qt::DisplayRole).toString(), row[1]->data(Qt::CheckStateRole).toBool(), - row[2]->data(Qt::CheckStateRole).toBool(), row[3]->data(Qt::CheckStateRole).toBool()}; + return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(), + row[1]->data(Qt::CheckStateRole).toBool(), + row[2]->data(Qt::CheckStateRole).toBool(), + row[3]->data(Qt::CheckStateRole).toBool()}; } // turns a row in the model into a vector item -void HighlightModel::getRowFromItem(const HighlightPhrase &item, std::vector &row) +void HighlightModel::getRowFromItem(const HighlightPhrase &item, + std::vector &row) { setStringItem(row[0], item.getPattern()); setBoolItem(row[1], item.getAlert()); @@ -35,31 +37,38 @@ void HighlightModel::getRowFromItem(const HighlightPhrase &item, std::vector row = this->createRow(); - setBoolItem(row[0], getApp()->settings->enableHighlightsSelf.getValue(), true, false); + setBoolItem(row[0], getApp()->settings->enableHighlightsSelf.getValue(), + true, false); row[0]->setData("Your username (automatic)", Qt::DisplayRole); - setBoolItem(row[1], getApp()->settings->enableHighlightTaskbar.getValue(), true, false); - setBoolItem(row[2], getApp()->settings->enableHighlightSound.getValue(), true, false); + setBoolItem(row[1], getApp()->settings->enableHighlightTaskbar.getValue(), + true, false); + setBoolItem(row[2], getApp()->settings->enableHighlightSound.getValue(), + true, false); row[3]->setFlags(0); this->insertCustomRow(row, 0); } -void HighlightModel::customRowSetData(const std::vector &row, int column, - const QVariant &value, int role) +void HighlightModel::customRowSetData(const std::vector &row, + int column, const QVariant &value, + int role) { switch (column) { case 0: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightsSelf.setValue(value.toBool()); + getApp()->settings->enableHighlightsSelf.setValue( + value.toBool()); } } break; case 1: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightTaskbar.setValue(value.toBool()); + getApp()->settings->enableHighlightTaskbar.setValue( + value.toBool()); } } break; case 2: { if (role == Qt::CheckStateRole) { - getApp()->settings->enableHighlightSound.setValue(value.toBool()); + getApp()->settings->enableHighlightSound.setValue( + value.toBool()); } } break; case 3: { diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp index 7f50287ed..b42246167 100644 --- a/src/controllers/highlights/HighlightModel.hpp +++ b/src/controllers/highlights/HighlightModel.hpp @@ -15,8 +15,9 @@ class HighlightModel : public SignalVectorModel protected: // turn a vector item into a model row - virtual HighlightPhrase getItemFromRow(std::vector &row, - const HighlightPhrase &original) override; + virtual HighlightPhrase getItemFromRow( + std::vector &row, + const HighlightPhrase &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const HighlightPhrase &item, @@ -24,8 +25,9 @@ protected: virtual void afterInit() override; - virtual void customRowSetData(const std::vector &row, int column, - const QVariant &value, int role) override; + virtual void customRowSetData(const std::vector &row, + int column, const QVariant &value, + int role) override; friend class HighlightController; }; diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index fe4ed5cc2..91abaadce 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -14,16 +14,20 @@ class HighlightPhrase public: bool operator==(const HighlightPhrase &other) const { - return std::tie(this->pattern_, this->sound_, this->alert_, this->isRegex_) == - std::tie(other.pattern_, other.sound_, other.alert_, other.isRegex_); + return std::tie(this->pattern_, this->sound_, this->alert_, + this->isRegex_) == std::tie(other.pattern_, + other.sound_, other.alert_, + other.isRegex_); } - HighlightPhrase(const QString &pattern, bool alert, bool sound, bool isRegex) + HighlightPhrase(const QString &pattern, bool alert, bool sound, + bool isRegex) : pattern_(pattern) , alert_(alert) , sound_(sound) , isRegex_(isRegex) - , regex_(isRegex_ ? pattern : "\\b" + QRegularExpression::escape(pattern) + "\\b", + , regex_(isRegex_ ? pattern + : "\\b" + QRegularExpression::escape(pattern) + "\\b", QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption) { diff --git a/src/controllers/highlights/UserHighlightModel.cpp b/src/controllers/highlights/UserHighlightModel.cpp index 92b862d60..e04c3403e 100644 --- a/src/controllers/highlights/UserHighlightModel.cpp +++ b/src/controllers/highlights/UserHighlightModel.cpp @@ -13,14 +13,15 @@ UserHighlightModel::UserHighlightModel(QObject *parent) } // turn vector item into model row -HighlightPhrase UserHighlightModel::getItemFromRow(std::vector &row, - const HighlightPhrase &original) +HighlightPhrase UserHighlightModel::getItemFromRow( + std::vector &row, const HighlightPhrase &original) { // key, regex - return HighlightPhrase{ - row[0]->data(Qt::DisplayRole).toString(), row[1]->data(Qt::CheckStateRole).toBool(), - row[2]->data(Qt::CheckStateRole).toBool(), row[3]->data(Qt::CheckStateRole).toBool()}; + return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(), + row[1]->data(Qt::CheckStateRole).toBool(), + row[2]->data(Qt::CheckStateRole).toBool(), + row[3]->data(Qt::CheckStateRole).toBool()}; } // row into vector item diff --git a/src/controllers/highlights/UserHighlightModel.hpp b/src/controllers/highlights/UserHighlightModel.hpp index b6a4023be..dcc42a950 100644 --- a/src/controllers/highlights/UserHighlightModel.hpp +++ b/src/controllers/highlights/UserHighlightModel.hpp @@ -15,8 +15,9 @@ class UserHighlightModel : public SignalVectorModel protected: // vector into model row - virtual HighlightPhrase getItemFromRow(std::vector &row, - const HighlightPhrase &original) override; + virtual HighlightPhrase getItemFromRow( + std::vector &row, + const HighlightPhrase &original) override; virtual void getRowFromItem(const HighlightPhrase &item, std::vector &row) override; diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp index 5aa271cb9..819779eff 100644 --- a/src/controllers/ignores/IgnoreController.cpp +++ b/src/controllers/ignores/IgnoreController.cpp @@ -7,7 +7,7 @@ namespace chatterino { -void IgnoreController::initialize(Application &) +void IgnoreController::initialize(Settings &, Paths &) { assert(!this->initialized_); this->initialized_ = true; diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index f03460131..e7a0472e6 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -8,12 +8,15 @@ namespace chatterino { +class Settings; +class Paths; + class IgnoreModel; -class IgnoreController : public Singleton +class IgnoreController final : public Singleton { public: - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector phrases; @@ -22,7 +25,8 @@ public: private: bool initialized_ = false; - ChatterinoSetting> ignoresSetting_ = {"/ignore/phrases"}; + ChatterinoSetting> ignoresSetting_ = { + "/ignore/phrases"}; }; } // namespace chatterino diff --git a/src/controllers/ignores/IgnoreModel.cpp b/src/controllers/ignores/IgnoreModel.cpp index f12fdfe6d..efa090290 100644 --- a/src/controllers/ignores/IgnoreModel.cpp +++ b/src/controllers/ignores/IgnoreModel.cpp @@ -25,7 +25,8 @@ IgnorePhrase IgnoreModel::getItemFromRow(std::vector &row, } // turns a row in the model into a vector item -void IgnoreModel::getRowFromItem(const IgnorePhrase &item, std::vector &row) +void IgnoreModel::getRowFromItem(const IgnorePhrase &item, + std::vector &row) { setStringItem(row[0], item.getPattern()); setBoolItem(row[1], item.isRegex()); diff --git a/src/controllers/ignores/IgnorePhrase.hpp b/src/controllers/ignores/IgnorePhrase.hpp index 428022a2e..c39c898b6 100644 --- a/src/controllers/ignores/IgnorePhrase.hpp +++ b/src/controllers/ignores/IgnorePhrase.hpp @@ -1,9 +1,10 @@ #pragma once #include "Application.hpp" -#include "common/SerializeCustom.hpp" #include "controllers/accounts/AccountController.hpp" #include "singletons/Settings.hpp" + +#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" #include @@ -101,7 +102,7 @@ private: bool isBlock_; QString replace_; bool isCaseSensitive_; - std::map emotes; + std::map emotes; }; } // namespace chatterino diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index a7312147f..49d302b34 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -1,5 +1,6 @@ #include "ModerationAction.hpp" +#include #include "Application.hpp" #include "singletons/Resources.hpp" @@ -12,7 +13,8 @@ namespace chatterino { //{ //} -// ModerationAction::ModerationAction(const QString &_line1, const QString &_line2, +// ModerationAction::ModerationAction(const QString &_line1, const QString +// &_line2, // const QString &_action) // : _isImage(false) // , image(nullptr) @@ -32,8 +34,8 @@ ModerationAction::ModerationAction(const QString &action) if (timeoutMatch.hasMatch()) { // if (multipleTimeouts > 1) { - QString line1; - QString line2; + // QString line1; + // QString line2; int amount = timeoutMatch.captured(1).toInt(); @@ -51,13 +53,14 @@ ModerationAction::ModerationAction(const QString &action) this->line2_ = "d"; } - this->line1_ = line1; - this->line2_ = line2; + // line1 = this->line1_; + // line2 = this->line2_; // } else { - // this->_moderationActions.emplace_back(app->resources->buttonTimeout, str); + // this->_moderationActions.emplace_back(app->resources->buttonTimeout, + // str); // } } else if (action.startsWith("/ban ")) { - this->image_ = getApp()->resources->buttonBan; + this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban); } else { QString xD = action; @@ -75,10 +78,10 @@ bool ModerationAction::operator==(const ModerationAction &other) const bool ModerationAction::isImage() const { - return this->image_ != nullptr; + return bool(this->image_); } -Image *ModerationAction::getImage() const +const boost::optional &ModerationAction::getImage() const { return this->image_; } diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp index bde1040c9..f509f79c5 100644 --- a/src/controllers/moderationactions/ModerationAction.hpp +++ b/src/controllers/moderationactions/ModerationAction.hpp @@ -1,14 +1,14 @@ #pragma once #include +#include #include +#include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { -class Image; - class ModerationAction { public: @@ -17,13 +17,13 @@ public: bool operator==(const ModerationAction &other) const; bool isImage() const; - Image *getImage() const; + const boost::optional &getImage() const; const QString &getLine1() const; const QString &getLine2() const; const QString &getAction() const; private: - Image *image_ = nullptr; + boost::optional image_; QString line1_; QString line2_; QString action_; diff --git a/src/controllers/moderationactions/ModerationActionModel.cpp b/src/controllers/moderationactions/ModerationActionModel.cpp index aa0fe134c..39f093713 100644 --- a/src/controllers/moderationactions/ModerationActionModel.cpp +++ b/src/controllers/moderationactions/ModerationActionModel.cpp @@ -11,8 +11,8 @@ ModerationActionModel ::ModerationActionModel(QObject *parent) } // turn a vector item into a model row -ModerationAction ModerationActionModel::getItemFromRow(std::vector &row, - const ModerationAction &original) +ModerationAction ModerationActionModel::getItemFromRow( + std::vector &row, const ModerationAction &original) { return ModerationAction(row[0]->data(Qt::DisplayRole).toString()); } diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp index 3c8a11675..3610561a9 100644 --- a/src/controllers/moderationactions/ModerationActionModel.hpp +++ b/src/controllers/moderationactions/ModerationActionModel.hpp @@ -16,8 +16,9 @@ public: protected: // turn a vector item into a model row - virtual ModerationAction getItemFromRow(std::vector &row, - const ModerationAction &original) override; + virtual ModerationAction getItemFromRow( + std::vector &row, + const ModerationAction &original) override; // turns a row in the model into a vector item virtual void getRowFromItem(const ModerationAction &item, diff --git a/src/controllers/moderationactions/ModerationActions.cpp b/src/controllers/moderationactions/ModerationActions.cpp index c130e65ad..68070f6b2 100644 --- a/src/controllers/moderationactions/ModerationActions.cpp +++ b/src/controllers/moderationactions/ModerationActions.cpp @@ -12,7 +12,7 @@ ModerationActions::ModerationActions() { } -void ModerationActions::initialize(Application &app) +void ModerationActions::initialize(Settings &settings, Paths &paths) { assert(!this->initialized_); this->initialized_ = true; diff --git a/src/controllers/moderationactions/ModerationActions.hpp b/src/controllers/moderationactions/ModerationActions.hpp index 934e2f95e..d5e510f1c 100644 --- a/src/controllers/moderationactions/ModerationActions.hpp +++ b/src/controllers/moderationactions/ModerationActions.hpp @@ -8,6 +8,9 @@ namespace chatterino { +class Settings; +class Paths; + class ModerationActionModel; class ModerationActions final : public Singleton @@ -15,14 +18,15 @@ class ModerationActions final : public Singleton public: ModerationActions(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; UnsortedSignalVector items; ModerationActionModel *createModel(QObject *parent); private: - ChatterinoSetting> setting_ = {"/moderation/actions"}; + ChatterinoSetting> setting_ = { + "/moderation/actions"}; bool initialized_ = false; }; diff --git a/src/controllers/taggedusers/TaggedUser.cpp b/src/controllers/taggedusers/TaggedUser.cpp index 9694c4fe0..def7de600 100644 --- a/src/controllers/taggedusers/TaggedUser.cpp +++ b/src/controllers/taggedusers/TaggedUser.cpp @@ -4,7 +4,8 @@ namespace chatterino { -TaggedUser::TaggedUser(ProviderId provider, const QString &name, const QString &id) +TaggedUser::TaggedUser(ProviderId provider, const QString &name, + const QString &id) : providerId_(provider) , name_(name) , id_(id) diff --git a/src/controllers/taggedusers/TaggedUsersController.hpp b/src/controllers/taggedusers/TaggedUsersController.hpp index cfd98b620..263ff391c 100644 --- a/src/controllers/taggedusers/TaggedUsersController.hpp +++ b/src/controllers/taggedusers/TaggedUsersController.hpp @@ -9,7 +9,7 @@ namespace chatterino { class TaggedUsersModel; -class TaggedUsersController : public Singleton +class TaggedUsersController final : public Singleton { public: TaggedUsersController(); diff --git a/src/controllers/taggedusers/TaggedUsersModel.cpp b/src/controllers/taggedusers/TaggedUsersModel.cpp index b3ef872f9..e3225888c 100644 --- a/src/controllers/taggedusers/TaggedUsersModel.cpp +++ b/src/controllers/taggedusers/TaggedUsersModel.cpp @@ -19,7 +19,8 @@ TaggedUser TaggedUsersModel::getItemFromRow(std::vector &row, } // turns a row in the model into a vector item -void TaggedUsersModel::getRowFromItem(const TaggedUser &item, std::vector &row) +void TaggedUsersModel::getRowFromItem(const TaggedUser &item, + std::vector &row) { setStringItem(row[0], item.getName()); } @@ -27,14 +28,18 @@ void TaggedUsersModel::getRowFromItem(const TaggedUser &item, std::vector row = this->createRow(); - // setBoolItem(row[0], getApp()->settings->enableHighlightsSelf.getValue(), true, - // false); row[0]->setData("Your username (automatic)", Qt::DisplayRole); - // setBoolItem(row[1], getApp()->settings->enableHighlightTaskbar.getValue(), true, - // false); setBoolItem(row[2], getApp()->settings->enableHighlightSound.getValue(), - // true, false); row[3]->setFlags(0); this->insertCustomRow(row, 0); + // setBoolItem(row[0], + // getApp()->settings->enableHighlightsSelf.getValue(), true, false); + // row[0]->setData("Your username (automatic)", Qt::DisplayRole); + // setBoolItem(row[1], + // getApp()->settings->enableHighlightTaskbar.getValue(), true, false); + // setBoolItem(row[2], + // getApp()->settings->enableHighlightSound.getValue(), true, false); + // row[3]->setFlags(0); this->insertCustomRow(row, 0); } -// void TaggedUserModel::customRowSetData(const std::vector &row, int column, +// void TaggedUserModel::customRowSetData(const std::vector +// &row, int column, // const QVariant &value, int role) //{ // switch (column) { diff --git a/src/controllers/taggedusers/TaggedUsersModel.hpp b/src/controllers/taggedusers/TaggedUsersModel.hpp index 44b566e5b..28c89493c 100644 --- a/src/controllers/taggedusers/TaggedUsersModel.hpp +++ b/src/controllers/taggedusers/TaggedUsersModel.hpp @@ -17,12 +17,15 @@ protected: const TaggedUser &original) override; // turns a row in the model into a vector item - virtual void getRowFromItem(const TaggedUser &item, std::vector &row) override; + virtual void getRowFromItem(const TaggedUser &item, + std::vector &row) override; virtual void afterInit() override; - // virtual void customRowSetData(const std::vector &row, int column, - // const QVariant &value, int role) override; + // virtual void customRowSetData(const std::vector &row, + // int column, + // const QVariant &value, int role) + // override; friend class TaggedUsersController; }; diff --git a/src/debug/Benchmark.cpp b/src/debug/Benchmark.cpp new file mode 100644 index 000000000..2ab28a680 --- /dev/null +++ b/src/debug/Benchmark.cpp @@ -0,0 +1,21 @@ +#include "Benchmark.hpp" + +namespace chatterino { + +BenchmarkGuard::BenchmarkGuard(const QString &_name) + : name_(_name) +{ + timer_.start(); +} + +BenchmarkGuard::~BenchmarkGuard() +{ + log("{} {} ms", this->name_, float(timer_.nsecsElapsed()) / 1000000.0f); +} + +qreal BenchmarkGuard::getElapsedMs() +{ + return qreal(timer_.nsecsElapsed()) / 1000000.0; +} + +} // namespace chatterino diff --git a/src/debug/Benchmark.hpp b/src/debug/Benchmark.hpp index c648382ea..065048e8f 100644 --- a/src/debug/Benchmark.hpp +++ b/src/debug/Benchmark.hpp @@ -1,41 +1,22 @@ #pragma once -#include +#include "debug/Log.hpp" + #include -#include #include -#define BENCH(x) \ - QElapsedTimer x; \ - x.start(); - -#define MARK(x) \ - qDebug() << BOOST_CURRENT_FUNCTION << __LINE__ \ - << static_cast(x.nsecsElapsed()) / 1000000.0 << "ms"; - namespace chatterino { class BenchmarkGuard : boost::noncopyable { - QElapsedTimer timer; - QString name; - public: - BenchmarkGuard(const QString &_name) - : name(_name) - { - timer.start(); - } + BenchmarkGuard(const QString &_name); + ~BenchmarkGuard(); + qreal getElapsedMs(); - ~BenchmarkGuard() - { - qDebug() << this->name << float(timer.nsecsElapsed()) / 1000000.0f << "ms"; - } - - qreal getElapsedMs() - { - return qreal(timer.nsecsElapsed()) / 1000000.0; - } +private: + QElapsedTimer timer_; + QString name_; }; } // namespace chatterino diff --git a/src/debug/Log.hpp b/src/debug/Log.hpp index 6889139ee..31536781a 100644 --- a/src/debug/Log.hpp +++ b/src/debug/Log.hpp @@ -8,10 +8,22 @@ namespace chatterino { template -inline void Log(const std::string &formatString, Args &&... args) +inline void log(const std::string &formatString, Args &&... args) { qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz") << fS(formatString, std::forward(args)...).c_str(); } +template +inline void log(const char *formatString, Args &&... args) +{ + log(std::string(formatString), std::forward(args)...); +} + +template +inline void log(const QString &formatString, Args &&... args) +{ + log(formatString.toStdString(), std::forward(args)...); +} + } // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index aa7e53aca..4bf6e7daf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,221 +1,33 @@ -#include "Application.hpp" -#include "common/NetworkManager.hpp" -#include "singletons/NativeMessaging.hpp" +#include "BrowserExtension.hpp" +#include "RunGui.hpp" #include "singletons/Paths.hpp" -#include "singletons/Updates.hpp" -#include "util/DebugCount.hpp" -#include "widgets/dialogs/LastRunCrashDialog.hpp" +#include "singletons/Settings.hpp" -#include #include -#include -#include #include -#include -#include +#include -#include -#include +using namespace chatterino; -#ifdef Q_OS_WIN -#include -#include -#include -#endif - -#ifdef C_USE_BREAKPAD -#include -#endif - -int runGui(QApplication &a, int argc, char *argv[]); -void runNativeMessagingHost(); -void installCustomPalette(); - -// -// Main entry point of the application. -// Decides if it should run in gui mode, daemon mode, ... -// Sets up the QApplication -// -int main(int argc, char *argv[]) +int main(int argc, char **argv) { - // set up the QApplication flags - QApplication::setAttribute(Qt::AA_Use96Dpi, true); -#ifdef Q_OS_WIN32 - QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); -#endif - // QApplication::setAttribute(Qt::AA_UseSoftwareOpenGL, true); + auto shared = std::make_shared(); + log(std::atomic_is_lock_free(&shared)); - // instanciate the QApplication QApplication a(argc, argv); - // FOURTF: might get arguments from the commandline passed in the future - chatterino::Paths::initInstance(); + // convert char** to QStringList + auto args = QStringList(); + std::transform(argv + 1, argv + argc, std::back_inserter(args), + [&](auto s) { return s; }); - // read args - QStringList args; - - for (int i = 1; i < argc; i++) { - args << argv[i]; - } - - // run native messaging host for the browser extension - if (args.size() > 0 && - (args[0].startsWith("chrome-extension://") || args[0].endsWith(".json"))) { - runNativeMessagingHost(); - return 0; - } - - // run gui - return runGui(a, argc, argv); -} - -int runGui(QApplication &a, int argc, char *argv[]) -{ - QApplication::setStyle(QStyleFactory::create("Fusion")); - - installCustomPalette(); - - // Initialize NetworkManager - chatterino::NetworkManager::init(); - - // Check for upates - chatterino::Updates::getInstance().checkForUpdates(); - - // Initialize application - chatterino::Application::instantiate(argc, argv); - auto app = chatterino::getApp(); - - app->construct(); - -#ifdef C_USE_BREAKPAD - QBreakpadInstance.setDumpPath(app->paths->settingsFolderPath + "/Crashes"); -#endif - - auto &pathMan = *app->paths; - // Running file - auto runningPath = pathMan.miscDirectory + "/running_" + pathMan.applicationFilePathHash; - - if (QFile::exists(runningPath)) { -#ifndef C_DISABLE_CRASH_DIALOG - chatterino::LastRunCrashDialog dialog; - - switch (dialog.exec()) { - case QDialog::Accepted: { - }; break; - default: { - _exit(0); - } - } -#endif + // run in gui mode or browser extension host mode + if (shouldRunBrowserExtensionHost(args)) { + runBrowserExtensionHost(); } else { - QFile runningFile(runningPath); + Paths paths; + Settings settings(paths); - runningFile.open(QIODevice::WriteOnly | QIODevice::Truncate); - runningFile.flush(); - runningFile.close(); - } - - app->initialize(); - - // Start the application - // This is a blocking call - app->run(a); - - // We have finished our application, make sure we save stuff - app->save(); - - // Running file - QFile::remove(runningPath); - - // Save settings - pajlada::Settings::SettingManager::save(); - - // Deinitialize NetworkManager (stop thread and wait for finish, should be instant) - chatterino::NetworkManager::deinit(); - - // None of the singletons has a proper destructor - _exit(0); -} - -void runNativeMessagingHost() -{ - auto *nm = new chatterino::NativeMessaging; - -#ifdef Q_OS_WIN - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -#endif - -#if 0 - bool bigEndian = isBigEndian(); -#endif - - std::atomic ping(false); - - QTimer timer; - QObject::connect(&timer, &QTimer::timeout, [&ping] { - if (!ping.exchange(false)) { - _exit(0); - } - }); - timer.setInterval(11000); - timer.start(); - - while (true) { - char size_c[4]; - std::cin.read(size_c, 4); - - if (std::cin.eof()) { - break; - } - - uint32_t size = *reinterpret_cast(size_c); -#if 0 - // To avoid breaking strict-aliasing rules and potentially inducing undefined behaviour, the following code can be run instead - uint32_t size = 0; - if (bigEndian) { - size = size_c[3] | static_cast(size_c[2]) << 8 | - static_cast(size_c[1]) << 16 | static_cast(size_c[0]) << 24; - } else { - size = size_c[0] | static_cast(size_c[1]) << 8 | - static_cast(size_c[2]) << 16 | static_cast(size_c[3]) << 24; - } -#endif - - std::unique_ptr b(new char[size + 1]); - std::cin.read(b.get(), size); - *(b.get() + size) = '\0'; - - nm->sendToGuiProcess(QByteArray::fromRawData(b.get(), static_cast(size))); + runGui(a, paths, settings); } } - -void installCustomPalette() -{ - // borrowed from - // https://stackoverflow.com/questions/15035767/is-the-qt-5-dark-fusion-theme-available-for-windows - QPalette darkPalette = qApp->palette(); - - darkPalette.setColor(QPalette::Window, QColor(22, 22, 22)); - darkPalette.setColor(QPalette::WindowText, Qt::white); - darkPalette.setColor(QPalette::Text, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::WindowText, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Base, QColor("#333")); - darkPalette.setColor(QPalette::AlternateBase, QColor("#444")); - darkPalette.setColor(QPalette::ToolTipBase, Qt::white); - darkPalette.setColor(QPalette::ToolTipText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::Dark, QColor(35, 35, 35)); - darkPalette.setColor(QPalette::Shadow, QColor(20, 20, 20)); - darkPalette.setColor(QPalette::Button, QColor(70, 70, 70)); - darkPalette.setColor(QPalette::ButtonText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); - darkPalette.setColor(QPalette::BrightText, Qt::red); - darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); - darkPalette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(80, 80, 80)); - darkPalette.setColor(QPalette::HighlightedText, Qt::white); - darkPalette.setColor(QPalette::Disabled, QPalette::HighlightedText, QColor(127, 127, 127)); - - qApp->setPalette(darkPalette); -} diff --git a/src/messages/Emote.cpp b/src/messages/Emote.cpp new file mode 100644 index 000000000..318e2d300 --- /dev/null +++ b/src/messages/Emote.cpp @@ -0,0 +1,45 @@ +#include "Emote.hpp" + +#include + +namespace chatterino { + +bool operator==(const Emote &a, const Emote &b) +{ + return std::tie(a.homePage, a.name, a.tooltip, a.images) == + std::tie(b.homePage, b.name, b.tooltip, b.images); +} + +bool operator!=(const Emote &a, const Emote &b) +{ + return !(a == b); +} + +EmotePtr cachedOrMakeEmotePtr(Emote &&emote, const EmoteMap &cache) +{ + // reuse old shared_ptr if nothing changed + auto it = cache.find(emote.name); + if (it != cache.end() && *it->second == emote) return it->second; + + return std::make_shared(std::move(emote)); +} + +EmotePtr cachedOrMakeEmotePtr( + Emote &&emote, + std::unordered_map> &cache, + std::mutex &mutex, const EmoteId &id) +{ + std::lock_guard guard(mutex); + + auto shared = cache[id].lock(); + if (shared && *shared == emote) { + // reuse old shared_ptr if nothing changed + return shared; + } else { + shared = std::make_shared(std::move(emote)); + cache[id] = shared; + return shared; + } +} + +} // namespace chatterino diff --git a/src/messages/Emote.hpp b/src/messages/Emote.hpp new file mode 100644 index 000000000..89cadc623 --- /dev/null +++ b/src/messages/Emote.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "messages/Image.hpp" +#include "messages/ImageSet.hpp" + +#include +#include +#include + +QStringAlias(EmoteId); +QStringAlias(EmoteName); + +namespace chatterino { + +struct Emote { + EmoteName name; + ImageSet images; + Tooltip tooltip; + Url homePage; + + // FOURTF: no solution yet, to be refactored later + const QString &getCopyString() const + { + return name.string; + } +}; + +bool operator==(const Emote &a, const Emote &b); +bool operator!=(const Emote &a, const Emote &b); + +using EmotePtr = std::shared_ptr; + +class EmoteMap : public std::unordered_map +{ +}; +using EmoteIdMap = std::unordered_map; +using WeakEmoteMap = std::unordered_map>; +using WeakEmoteIdMap = std::unordered_map>; + +EmotePtr cachedOrMakeEmotePtr(Emote &&emote, const EmoteMap &cache); +EmotePtr cachedOrMakeEmotePtr( + Emote &&emote, + std::unordered_map> &cache, + std::mutex &mutex, const EmoteId &id); + +} // namespace chatterino diff --git a/src/messages/EmoteCache.hpp b/src/messages/EmoteCache.hpp new file mode 100644 index 000000000..1bbb01fd4 --- /dev/null +++ b/src/messages/EmoteCache.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include + +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +template +class MapReplacement +{ +public: + MapReplacement(std::unordered_map &items) + : oldItems_(items) + { + } + + void add(const TKey &key, const Emote &data) + { + this->add(key, Emote(data)); + } + + void add(const TKey &key, Emote &&data) + { + auto it = this->oldItems_.find(key); + if (it != this->oldItems_.end() && *it->second == data) { + this->newItems_[key] = it->second; + } else { + this->newItems_[key] = std::make_shared(std::move(data)); + } + } + + void apply() + { + this->oldItems_ = std::move(this->newItems_); + } + +private: + std::unordered_map &oldItems_; + std::unordered_map newItems_; +}; + +template +class EmoteCache +{ +public: + using Iterator = typename std::unordered_map::iterator; + using ConstIterator = typename std::unordered_map::iterator; + + Iterator begin() + { + return this->items_.begin(); + } + + ConstIterator begin() const + { + return this->items_.begin(); + } + + Iterator end() + { + return this->items_.end(); + } + + ConstIterator end() const + { + return this->items_.end(); + } + + boost::optional get(const TKey &key) const + { + auto it = this->items_.find(key); + + if (it == this->items_.end()) + return boost::none; + else + return it->second; + } + + MapReplacement makeReplacment() + { + return MapReplacement(this->items_); + } + +private: + std::unordered_map items_; +}; + +} // namespace chatterino diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 44b204329..572432cdd 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -1,11 +1,13 @@ #include "messages/Image.hpp" #include "Application.hpp" -#include "common/NetworkManager.hpp" -#include "common/UrlFetch.hpp" +#include "common/NetworkRequest.hpp" +#include "debug/AssertInGuiThread.hpp" +#include "debug/Benchmark.hpp" #include "debug/Log.hpp" #include "singletons/Emotes.hpp" #include "singletons/WindowManager.hpp" +#include "util/DebugCount.hpp" #include "util/PostToThread.hpp" #include @@ -14,264 +16,309 @@ #include #include #include - #include #include namespace chatterino { - -bool Image::loadedEventQueued = false; - -Image::Image(const QString &url, qreal scale, const QString &name, const QString &tooltip, - const QMargins &margin, bool isHat) - : url(url) - , name(name) - , tooltip(tooltip) - , margin(margin) - , ishat(isHat) - , scale(scale) +namespace { +// Frames +Frames::Frames() { DebugCount::increase("images"); } -Image::Image(QPixmap *image, qreal scale, const QString &name, const QString &tooltip, - const QMargins &margin, bool isHat) - : currentPixmap(image) - , name(name) - , tooltip(tooltip) - , margin(margin) - , ishat(isHat) - , scale(scale) - , isLoading(true) - , isLoaded(true) +Frames::Frames(const QVector> &frames) + : items_(frames) { + assertInGuiThread(); DebugCount::increase("images"); + + if (this->animated()) { + DebugCount::increase("animated images"); + + this->gifTimerConnection_ = getApp()->emotes->gifTimer.signal.connect( + [this] { this->advance(); }); + } } -Image::~Image() +Frames::~Frames() { + assertInGuiThread(); DebugCount::decrease("images"); - if (this->isAnimated()) { + if (this->animated()) { DebugCount::decrease("animated images"); } - if (this->isLoaded) { - DebugCount::decrease("loaded images"); + this->gifTimerConnection_.disconnect(); +} + +void Frames::advance() +{ + this->durationOffset_ += GIF_FRAME_LENGTH; + + while (true) { + this->index_ %= this->items_.size(); + + 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; + this->index_ = (this->index_ + 1) % this->items_.size(); + } else { + break; + } } } -void Image::loadImage() +bool Frames::animated() const { - NetworkRequest req(this->getUrl()); - req.setCaller(this); - req.setUseQuickLoadCache(true); - req.onSuccess([this](auto result) -> bool { - auto bytes = result.getData(); - QByteArray copy = QByteArray::fromRawData(bytes.constData(), bytes.length()); - QBuffer buffer(©); - buffer.open(QIODevice::ReadOnly); + return this->items_.size() > 1; +} - QImage image; - QImageReader reader(&buffer); +boost::optional Frames::current() const +{ + if (this->items_.size() == 0) return boost::none; + return this->items_[this->index_].image; +} - bool first = true; +boost::optional Frames::first() const +{ + if (this->items_.size() == 0) return boost::none; + return this->items_.front().image; +} - // clear stuff before loading the image again - this->allFrames.clear(); - if (this->isAnimated()) { - DebugCount::decrease("animated images"); +// functions +QVector> readFrames(QImageReader &reader, const Url &url) +{ + QVector> frames; + + if (reader.imageCount() == 0) { + log("Error while reading image {}: '{}'", url.string, + reader.errorString()); + return frames; + } + + QImage image; + for (int index = 0; index < reader.imageCount(); ++index) { + if (reader.read(&image)) { + QPixmap::fromImage(image); + + int duration = std::max(20, reader.nextImageDelay()); + frames.push_back(Frame{image, duration}); } - if (this->isLoaded) { - DebugCount::decrease("loaded images"); + } + + if (frames.size() == 0) { + log("Error while reading image {}: '{}'", url.string, + reader.errorString()); + } + + return frames; +} + +// parsed +template +void assignDelayed( + std::queue>>> &queued, + std::mutex &mutex, std::atomic_bool &loadedEventQueued) +{ + std::lock_guard lock(mutex); + int i = 0; + + while (!queued.empty()) { + queued.front().first(queued.front().second); + queued.pop(); + + if (++i > 50) { + QTimer::singleShot( + 3, [&] { assignDelayed(queued, mutex, loadedEventQueued); }); + return; } + } - if (reader.imageCount() == -1) { - // An error occured in the reader - Log("An error occured reading the image: '{}'", reader.errorString()); - Log("Image url: {}", this->url); - return false; - } + getApp()->windows->forceLayoutChannelViews(); + loadedEventQueued = false; +} - if (reader.imageCount() == 0) { - Log("Error: No images read in the buffer"); - // No images read in the buffer. maybe a cache error? - return false; - } +template +auto makeConvertCallback(const QVector> &parsed, Assign assign) +{ + return [parsed, assign] { + // convert to pixmap + auto frames = QVector>(); + std::transform(parsed.begin(), parsed.end(), std::back_inserter(frames), + [](auto &frame) { + return Frame{ + QPixmap::fromImage(frame.image), frame.duration}; + }); - for (int index = 0; index < reader.imageCount(); ++index) { - if (reader.read(&image)) { - auto pixmap = new QPixmap(QPixmap::fromImage(image)); + // put into stack + static std::queue>>> queued; + static std::mutex mutex; - if (first) { - first = false; - this->loadedPixmap = pixmap; - } + std::lock_guard lock(mutex); + queued.emplace(assign, frames); - Image::FrameData data; - data.duration = std::max(20, reader.nextImageDelay()); - data.image = pixmap; - - this->allFrames.push_back(data); - } - } - - if (this->allFrames.size() != reader.imageCount()) { - // Log("Error: Wrong amount of images read"); - // One or more images failed to load from the buffer - // return false; - } - - if (this->allFrames.size() > 1) { - if (!this->animated) { - postToThread([this] { - getApp()->emotes->gifTimer.signal.connect([=]() { - this->gifUpdateTimout(); - }); // For some reason when Boost signal is in - // thread scope and thread deletes the signal - // doesn't work, so this is the fix. - }); - } - - this->animated = true; - - DebugCount::increase("animated images"); - } - - this->currentPixmap = this->loadedPixmap; - - this->isLoaded = true; - DebugCount::increase("loaded images"); + static std::atomic_bool loadedEventQueued{false}; if (!loadedEventQueued) { loadedEventQueued = true; - QTimer::singleShot(500, [] { - getApp()->windows->incGeneration(); - - auto app = getApp(); - app->windows->layoutChannelViews(); - loadedEventQueued = false; - }); + QTimer::singleShot( + 100, [=] { assignDelayed(queued, mutex, loadedEventQueued); }); } + }; +} +} // namespace - return true; +// IMAGE2 +ImagePtr Image::fromUrl(const Url &url, qreal scale) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + std::lock_guard lock(mutex); + + auto shared = cache[url].lock(); + + if (!shared) { + cache[url] = shared = ImagePtr(new Image(url, scale)); + } else { + // Warn("same image loaded multiple times: {}", url.string); + } + + return shared; +} + +ImagePtr Image::fromPixmap(const QPixmap &pixmap, qreal scale) +{ + return ImagePtr(new Image(pixmap, scale)); +} + +ImagePtr Image::getEmpty() +{ + static auto empty = ImagePtr(new Image); + return empty; +} + +Image::Image() + : empty_(true) +{ +} + +Image::Image(const Url &url, qreal scale) + : url_(url) + , scale_(scale) + , shouldLoad_(true) + , frames_(std::make_unique()) +{ +} + +Image::Image(const QPixmap &pixmap, qreal scale) + : scale_(scale) + , frames_(std::make_unique( + QVector>{Frame{pixmap, 1}})) +{ +} + +const Url &Image::url() const +{ + return this->url_; +} + +boost::optional Image::pixmap() const +{ + assertInGuiThread(); + + if (this->shouldLoad_) { + const_cast(this)->shouldLoad_ = false; + const_cast(this)->load(); + } + + return this->frames_->current(); +} + +qreal Image::scale() const +{ + return this->scale_; +} + +bool Image::isEmpty() const +{ + return this->empty_; +} + +bool Image::animated() const +{ + assertInGuiThread(); + + return this->frames_->animated(); +} + +int Image::width() const +{ + assertInGuiThread(); + + if (auto pixmap = this->frames_->first()) + return pixmap->width() * this->scale_; + else + return 16; +} + +int Image::height() const +{ + assertInGuiThread(); + + if (auto pixmap = this->frames_->first()) + return pixmap->height() * this->scale_; + else + return 16; +} + +void Image::load() +{ + NetworkRequest req(this->url().string); + req.setExecuteConcurrently(true); + req.setCaller(&this->object_); + req.setUseQuickLoadCache(true); + req.onSuccess([that = this, weak = weakOf(this)](auto result) -> Outcome { + auto shared = weak.lock(); + if (!shared) return Failure; + + auto data = result.getData(); + + // const cast since we are only reading from it + QBuffer buffer(const_cast(&data)); + buffer.open(QIODevice::ReadOnly); + QImageReader reader(&buffer); + auto parsed = readFrames(reader, that->url()); + + postToThread(makeConvertCallback(parsed, [weak](auto frames) { + if (auto shared = weak.lock()) + shared->frames_ = std::make_unique(frames); + })); + + return Success; }); req.execute(); } -void Image::gifUpdateTimout() +bool Image::operator==(const Image &other) const { - if (this->animated) { - this->currentFrameOffset += GIF_FRAME_LENGTH; + if (this->isEmpty() && other.isEmpty()) return true; + if (!this->url_.string.isEmpty() && this->url_ == other.url_) return true; + if (this->frames_->first() == other.frames_->first()) return true; - while (true) { - if (this->currentFrameOffset > this->allFrames.at(this->currentFrame).duration) { - this->currentFrameOffset -= this->allFrames.at(this->currentFrame).duration; - this->currentFrame = (this->currentFrame + 1) % this->allFrames.size(); - } else { - break; - } - } - - this->currentPixmap = this->allFrames[this->currentFrame].image; - } + return false; } -const QPixmap *Image::getPixmap() +bool Image::operator!=(const Image &other) const { - if (!this->isLoading) { - this->isLoading = true; - - this->loadImage(); - - return nullptr; - } - - if (this->isLoaded) { - return this->currentPixmap; - } else { - return nullptr; - } -} - -qreal Image::getScale() const -{ - return this->scale; -} - -const QString &Image::getUrl() const -{ - return this->url; -} - -const QString &Image::getName() const -{ - return this->name; -} - -const QString &Image::getCopyString() const -{ - if (this->copyString.isEmpty()) { - return this->name; - } - - return this->copyString; -} - -const QString &Image::getTooltip() const -{ - return this->tooltip; -} - -const QMargins &Image::getMargin() const -{ - return this->margin; -} - -bool Image::isAnimated() const -{ - return this->animated; -} - -bool Image::isHat() const -{ - return this->ishat; -} - -int Image::getWidth() const -{ - if (this->currentPixmap == nullptr) { - return 16; - } - - return this->currentPixmap->width(); -} - -int Image::getScaledWidth() const -{ - return static_cast((float)this->getWidth() * this->scale * - getApp()->settings->emoteScale.getValue()); -} - -int Image::getHeight() const -{ - if (this->currentPixmap == nullptr) { - return 16; - } - return this->currentPixmap->height(); -} - -int Image::getScaledHeight() const -{ - return static_cast((float)this->getHeight() * this->scale * - getApp()->settings->emoteScale.getValue()); -} - -void Image::setCopyString(const QString &newCopyString) -{ - this->copyString = newCopyString; + return !this->operator==(other); } } // namespace chatterino diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index c25c4db45..3d869f798 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -1,69 +1,80 @@ #pragma once +#include "common/Common.hpp" + #include #include -#include - +#include +#include #include +#include +#include +#include +#include +#include + +#include "common/NullablePtr.hpp" namespace chatterino { - -class Image : public QObject, boost::noncopyable +namespace { +template +struct Frame { + Image image; + int duration; +}; +class Frames : boost::noncopyable { public: - explicit Image(const QString &_url, qreal _scale = 1, const QString &_name = "", - const QString &_tooltip = "", const QMargins &_margin = QMargins(), - bool isHat = false); + Frames(); + Frames(const QVector> &frames); + ~Frames(); - explicit Image(QPixmap *_currentPixmap, qreal _scale = 1, const QString &_name = "", - const QString &_tooltip = "", const QMargins &_margin = QMargins(), - bool isHat = false); - ~Image(); - - const QPixmap *getPixmap(); - qreal getScale() const; - const QString &getUrl() const; - const QString &getName() const; - const QString &getCopyString() const; - const QString &getTooltip() const; - const QMargins &getMargin() const; - bool isAnimated() const; - bool isHat() const; - int getWidth() const; - int getScaledWidth() const; - int getHeight() const; - int getScaledHeight() const; - - void setCopyString(const QString &newCopyString); + bool animated() const; + void advance(); + boost::optional current() const; + boost::optional first() const; private: - struct FrameData { - QPixmap *image; - int duration; - }; - - static bool loadedEventQueued; - - QPixmap *currentPixmap = nullptr; - QPixmap *loadedPixmap = nullptr; - std::vector allFrames; - int currentFrame = 0; - int currentFrameOffset = 0; - - QString url; - QString name; - QString copyString; - QString tooltip; - bool animated = false; - QMargins margin; - bool ishat; - qreal scale; - - bool isLoading = false; - std::atomic isLoaded{false}; - - void loadImage(); - void gifUpdateTimout(); + QVector> items_; + int index_{0}; + int durationOffset_{0}; + pajlada::Signals::Connection gifTimerConnection_; }; +} // namespace +class Image; +using ImagePtr = std::shared_ptr; + +class Image : public std::enable_shared_from_this, boost::noncopyable +{ +public: + static ImagePtr fromUrl(const Url &url, qreal scale = 1); + static ImagePtr fromPixmap(const QPixmap &pixmap, qreal scale = 1); + static ImagePtr getEmpty(); + + const Url &url() const; + boost::optional pixmap() const; + qreal scale() const; + bool isEmpty() const; + int width() const; + int height() const; + bool animated() const; + + bool operator==(const Image &image) const; + bool operator!=(const Image &image) const; + +private: + Image(); + Image(const Url &url, qreal scale); + Image(const QPixmap &nonOwning, qreal scale); + + void load(); + + Url url_{}; + qreal scale_{1}; + bool empty_{false}; + bool shouldLoad_{false}; + std::unique_ptr frames_{}; + QObject object_{}; +}; } // namespace chatterino diff --git a/src/messages/ImageSet.cpp b/src/messages/ImageSet.cpp new file mode 100644 index 000000000..0e500816f --- /dev/null +++ b/src/messages/ImageSet.cpp @@ -0,0 +1,96 @@ +#include "ImageSet.hpp" + +#include "Application.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +ImageSet::ImageSet() + : imageX1_(Image::getEmpty()) + , imageX2_(Image::getEmpty()) + , imageX3_(Image::getEmpty()) +{ +} + +ImageSet::ImageSet(const ImagePtr &image1, const ImagePtr &image2, + const ImagePtr &image3) + : imageX1_(image1) + , imageX2_(image2) + , imageX3_(image3) +{ +} + +ImageSet::ImageSet(const Url &image1, const Url &image2, const Url &image3) + : imageX1_(Image::fromUrl(image1, 1)) + , imageX2_(Image::fromUrl(image2, 0.5)) + , imageX3_(Image::fromUrl(image3, 0.25)) +{ +} + +void ImageSet::setImage1(const ImagePtr &image) +{ + this->imageX1_ = image; +} + +void ImageSet::setImage2(const ImagePtr &image) +{ + this->imageX2_ = image; +} + +void ImageSet::setImage3(const ImagePtr &image) +{ + this->imageX3_ = image; +} + +const ImagePtr &ImageSet::getImage1() const +{ + return this->imageX1_; +} + +const ImagePtr &ImageSet::getImage2() const +{ + return this->imageX2_; +} + +const ImagePtr &ImageSet::getImage3() const +{ + return this->imageX3_; +} + +const ImagePtr &ImageSet::getImage(float scale) const +{ + int quality = getSettings()->preferredEmoteQuality; + + if (!quality) { + if (scale > 3.999) + quality = 3; + else if (scale > 1.999) + quality = 2; + else + scale = 1; + } + + if (!this->imageX3_->isEmpty() && quality == 3) { + return this->imageX3_; + } + + if (!this->imageX2_->isEmpty() && quality == 2) { + return this->imageX3_; + } + + return this->imageX1_; +} + +bool ImageSet::operator==(const ImageSet &other) const +{ + return std::tie(this->imageX1_, this->imageX2_, this->imageX3_) == + std::tie(other.imageX1_, other.imageX2_, other.imageX3_); +} + +bool ImageSet::operator!=(const ImageSet &other) const +{ + return !this->operator==(other); +} + +} // namespace chatterino diff --git a/src/messages/ImageSet.hpp b/src/messages/ImageSet.hpp new file mode 100644 index 000000000..e8872b0b7 --- /dev/null +++ b/src/messages/ImageSet.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "messages/Image.hpp" + +namespace chatterino { + +class ImageSet +{ +public: + ImageSet(); + ImageSet(const ImagePtr &image1, const ImagePtr &image2 = Image::getEmpty(), + const ImagePtr &image3 = Image::getEmpty()); + ImageSet(const Url &image1, const Url &image2 = {}, const Url &image3 = {}); + + void setImage1(const ImagePtr &image); + void setImage2(const ImagePtr &image); + void setImage3(const ImagePtr &image); + const ImagePtr &getImage1() const; + const ImagePtr &getImage2() const; + const ImagePtr &getImage3() const; + + const ImagePtr &getImage(float scale) const; + + bool operator==(const ImageSet &other) const; + bool operator!=(const ImageSet &other) const; + +private: + ImagePtr imageX1_; + ImagePtr imageX2_; + ImagePtr imageX3_; +}; + +} // namespace chatterino diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 4870c6809..5165efa94 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -16,7 +16,8 @@ namespace chatterino { // // Explanation: // - messages can be appended until 'limit' is reached -// - when the limit is reached for every message added one will be removed at the start +// - when the limit is reached for every message added one will be removed at +// the start // - messages can only be added to the start when there is space for them, // trying to add messages to the start when it's full will not add them // - you are able to get a "Snapshot" which captures the state of this object @@ -41,7 +42,8 @@ public: { std::lock_guard lock(this->mutex_); - this->chunks_ = std::make_shared>>>(); + this->chunks_ = + std::make_shared>>>(); Chunk chunk = std::make_shared>(); chunk->resize(this->chunkSize_); this->chunks_->push_back(chunk); @@ -60,8 +62,8 @@ public: // still space in the last chunk if (lastChunk->size() <= this->lastChunkEnd_) { // create new chunk vector - ChunkVector newVector = - std::make_shared>>>(); + ChunkVector newVector = std::make_shared< + std::vector>>>(); // copy chunks for (Chunk &chunk : *this->chunks_) { @@ -93,8 +95,8 @@ public: std::lock_guard lock(this->mutex_); // create new vector to clone chunks into - ChunkVector newChunks = - std::make_shared>>>(); + ChunkVector newChunks = std::make_shared< + std::vector>>>(); newChunks->resize(this->chunks_->size()); @@ -142,7 +144,8 @@ public: Chunk &chunk = this->chunks_->at(i); size_t start = i == 0 ? this->firstChunkOffset_ : 0; - size_t end = i == chunk->size() - 1 ? this->lastChunkEnd_ : chunk->size(); + size_t end = + i == chunk->size() - 1 ? this->lastChunkEnd_ : chunk->size(); for (size_t j = start; j < end; j++) { if (chunk->at(j) == item) { @@ -176,7 +179,8 @@ public: Chunk &chunk = this->chunks_->at(i); size_t start = i == 0 ? this->firstChunkOffset_ : 0; - size_t end = i == chunk->size() - 1 ? this->lastChunkEnd_ : chunk->size(); + size_t end = + i == chunk->size() - 1 ? this->lastChunkEnd_ : chunk->size(); for (size_t j = start; j < end; j++) { if (x == index) { @@ -204,8 +208,9 @@ public: { std::lock_guard lock(this->mutex_); - return LimitedQueueSnapshot(this->chunks_, this->limit_ - this->space(), - this->firstChunkOffset_, this->lastChunkEnd_); + return LimitedQueueSnapshot( + this->chunks_, this->limit_ - this->space(), + this->firstChunkOffset_, this->lastChunkEnd_); } private: @@ -238,8 +243,8 @@ private: // need to delete the first chunk if (this->firstChunkOffset_ == this->chunks_->front()->size() - 1) { // copy the chunk vector - ChunkVector newVector = - std::make_shared>>>(); + ChunkVector newVector = std::make_shared< + std::vector>>>(); // delete first chunk bool first = true; diff --git a/src/messages/LimitedQueueSnapshot.hpp b/src/messages/LimitedQueueSnapshot.hpp index cd735445e..ce78dad94 100644 --- a/src/messages/LimitedQueueSnapshot.hpp +++ b/src/messages/LimitedQueueSnapshot.hpp @@ -12,8 +12,9 @@ class LimitedQueueSnapshot public: LimitedQueueSnapshot() = default; - LimitedQueueSnapshot(std::shared_ptr>>> chunks, - size_t length, size_t firstChunkOffset, size_t lastChunkEnd) + LimitedQueueSnapshot( + std::shared_ptr>>> chunks, + size_t length, size_t firstChunkOffset, size_t lastChunkEnd) : chunks_(chunks) , length_(length) , firstChunkOffset_(firstChunkOffset) diff --git a/src/messages/Link.hpp b/src/messages/Link.hpp index a07d21cf3..a6c503540 100644 --- a/src/messages/Link.hpp +++ b/src/messages/Link.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include namespace chatterino { diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index fcf5fbc9e..8fa860353 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -1,212 +1,37 @@ #include "messages/Message.hpp" #include "MessageElement.hpp" #include "providers/twitch/PubsubActions.hpp" +#include "util/DebugCount.hpp" #include "util/IrcHelpers.hpp" using SBHighlight = chatterino::ScrollbarHighlight; namespace chatterino { -void Message::addElement(MessageElement *element) +Message::Message() + : parseTime(QTime::currentTime()) { - this->elements_.push_back(std::unique_ptr(element)); + DebugCount::increase("messages"); } -const std::vector> &Message::getElements() const +Message::~Message() { - return this->elements_; + DebugCount::decrease("messages"); } SBHighlight Message::getScrollBarHighlight() const { - if (this->flags & Message::Highlighted) { + if (this->flags.has(MessageFlag::Highlighted)) { return SBHighlight(SBHighlight::Highlight); - } else if (this->flags & Message::Subscription) { + } else if (this->flags.has(MessageFlag::Subscription)) { return SBHighlight(SBHighlight::Subscription); } return SBHighlight(); } // Static -MessagePtr Message::createSystemMessage(const QString &text) -{ - MessagePtr message(new Message); - - message->addElement(new TimestampElement(QTime::currentTime())); - message->addElement(new TextElement(text, MessageElement::Text, MessageColor::System)); - message->flags |= MessageFlags::System; - message->flags |= MessageFlags::DoNotTriggerNotification; - message->searchText = text; - - return message; -} - -MessagePtr Message::createMessage(const QString &text) -{ - MessagePtr message(new Message); - - message->addElement(new TimestampElement(QTime::currentTime())); - message->addElement(new TextElement(text, MessageElement::Text, MessageColor::Text)); - message->searchText = text; - - return message; -} - namespace { -QString makeDuration(int count, const QString &order) -{ - QString text; - - text.append(QString::number(count)); - text.append(" " + order); - - if (count > 1) { - text.append("s"); - } - - return text; -} - -QString makeDuration(uint32_t timeoutSeconds) -{ - int timeoutMinutes = floor(timeoutSeconds / 60); - - if (timeoutMinutes > 0 && timeoutSeconds % 60 == 0) { - int timeoutHours = floor(timeoutMinutes / 60); - - if (timeoutHours > 0 && timeoutMinutes % 60 == 0) { - int timeoutDays = floor(timeoutHours / 24); - - if (timeoutDays > 0 && timeoutHours % 24 == 0) { - return makeDuration(timeoutDays, "day"); - } - - return makeDuration(timeoutHours, "hour"); - } - - return makeDuration(timeoutMinutes, "minute"); - } - - return makeDuration(timeoutSeconds, "second"); -} - } // namespace -MessagePtr Message::createTimeoutMessage(const QString &username, const QString &durationInSeconds, - const QString &reason, bool multipleTimes) -{ - QString text; - - text.append(username); - if (!durationInSeconds.isEmpty()) { - text.append(" has been timed out"); - - // TODO: Implement who timed the user out - - text.append(" for "); - bool ok = true; - int timeoutSeconds = durationInSeconds.toInt(&ok); - if (ok) { - text.append(makeDuration(timeoutSeconds)); - } - } else { - text.append(" has been permanently banned"); - } - - if (reason.length() > 0) { - text.append(": \""); - text.append(parseTagString(reason)); - text.append("\""); - } - text.append("."); - - if (multipleTimes) { - text.append(" (multiple times)"); - } - - MessagePtr message = Message::createSystemMessage(text); - message->flags.EnableFlag(MessageFlags::System); - message->flags.EnableFlag(MessageFlags::Timeout); - message->timeoutUser = username; - return message; -} - -MessagePtr Message::createTimeoutMessage(const BanAction &action, uint32_t count) -{ - MessagePtr msg(new Message); - - msg->addElement(new TimestampElement(QTime::currentTime())); - msg->flags.EnableFlag(MessageFlags::System); - msg->flags.EnableFlag(MessageFlags::Timeout); - - msg->timeoutUser = action.target.name; - msg->count = count; - - QString text; - - if (action.isBan()) { - 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 { - if (action.reason.isEmpty()) { - text = QString("%1 timed out %2 for %3.") // - .arg(action.source.name) - .arg(action.target.name) - .arg(makeDuration(action.duration)); - } else { - text = QString("%1 timed out %2 for %3: \"%4\".") // - .arg(action.source.name) - .arg(action.target.name) - .arg(makeDuration(action.duration)) - .arg(action.reason); - } - - if (count > 1) { - text.append(QString(" (%1 times)").arg(count)); - } - } - - msg->addElement(new TextElement(text, MessageElement::Text, MessageColor::System)); - msg->searchText = text; - - return msg; -} - -MessagePtr Message::createUntimeoutMessage(const UnbanAction &action) -{ - MessagePtr msg(new Message); - - msg->addElement(new TimestampElement(QTime::currentTime())); - msg->flags.EnableFlag(MessageFlags::System); - msg->flags.EnableFlag(MessageFlags::Untimeout); - - msg->timeoutUser = action.target.name; - - QString text; - - if (action.wasBan()) { - text = QString("%1 unbanned %2.") // - .arg(action.source.name) - .arg(action.target.name); - } else { - text = QString("%1 untimedout %2.") // - .arg(action.source.name) - .arg(action.target.name); - } - - msg->addElement(new TextElement(text, MessageElement::Text, MessageColor::System)); - msg->searchText = text; - - return msg; -} - } // namespace chatterino diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 102a8d32f..76039ec41 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -6,44 +6,34 @@ #include "widgets/helper/ScrollbarHighlight.hpp" #include - #include #include #include -#include "util/DebugCount.hpp" - namespace chatterino { -struct Message { - Message() - : parseTime(QTime::currentTime()) - { - DebugCount::increase("messages"); - } +enum class MessageFlag : uint16_t { + None = 0, + System = (1 << 0), + Timeout = (1 << 1), + Highlighted = (1 << 2), + DoNotTriggerNotification = (1 << 3), // disable notification sound + Centered = (1 << 4), + Disabled = (1 << 5), + DisableCompactEmotes = (1 << 6), + Collapsed = (1 << 7), + DisconnectedMessage = (1 << 8), + Untimeout = (1 << 9), + PubSub = (1 << 10), + Subscription = (1 << 11), +}; +using MessageFlags = FlagsEnum; - ~Message() - { - DebugCount::decrease("messages"); - } +struct Message : boost::noncopyable { + Message(); + ~Message(); - enum MessageFlags : uint16_t { - None = 0, - System = (1 << 0), - Timeout = (1 << 1), - Highlighted = (1 << 2), - DoNotTriggerNotification = (1 << 3), // disable notification sound - Centered = (1 << 4), - Disabled = (1 << 5), - DisableCompactEmotes = (1 << 6), - Collapsed = (1 << 7), - DisconnectedMessage = (1 << 8), - Untimeout = (1 << 9), - PubSub = (1 << 10), - Subscription = (1 << 11), - }; - - FlagsEnum flags; + MessageFlags flags; QTime parseTime; QString id; QString searchText; @@ -51,32 +41,12 @@ struct Message { QString displayName; QString localizedName; QString timeoutUser; - uint32_t count = 1; + std::vector> elements; - // Messages should not be added after the message is done initializing. - void addElement(MessageElement *element); - const std::vector> &getElements() const; - - // Scrollbar ScrollbarHighlight getScrollBarHighlight() const; - -private: - std::vector> elements_; - -public: - static std::shared_ptr createSystemMessage(const QString &text); - static std::shared_ptr createMessage(const QString &text); - - static std::shared_ptr createTimeoutMessage(const QString &username, - const QString &durationInSeconds, - const QString &reason, bool multipleTimes); - - static std::shared_ptr createTimeoutMessage(const BanAction &action, - uint32_t count = 1); - static std::shared_ptr createUntimeoutMessage(const UnbanAction &action); }; -using MessagePtr = std::shared_ptr; +using MessagePtr = std::shared_ptr; } // namespace chatterino diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 0054b887d..f4ebd2301 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -4,51 +4,185 @@ #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Theme.hpp" +#include "util/FormatTime.hpp" +#include "util/IrcHelpers.hpp" #include namespace chatterino { +MessagePtr makeSystemMessage(const QString &text) +{ + return MessageBuilder(systemMessage, text).release(); +} + MessageBuilder::MessageBuilder() - : message_(new Message) + : message_(std::make_unique()) { } -MessagePtr MessageBuilder::getMessage() +MessageBuilder::MessageBuilder(const QString &text) + : MessageBuilder() { - return this->message_; + this->emplace(); + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + this->message().searchText = text; } -void MessageBuilder::append(MessageElement *element) +MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text) + : MessageBuilder() { - this->message_->addElement(element); + this->emplace(); + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); + this->message().searchText = text; } -void MessageBuilder::appendTimestamp() +MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, + const QString &durationInSeconds, + const QString &reason, bool multipleTimes) + : MessageBuilder() { - this->appendTimestamp(QTime::currentTime()); -} + QString text; -void MessageBuilder::setHighlight(bool value) -{ - if (value) { - this->message_->flags |= Message::Highlighted; + text.append(username); + if (!durationInSeconds.isEmpty()) { + text.append(" has been timed out"); + + // TODO: Implement who timed the user out + + text.append(" for "); + bool ok = true; + int timeoutSeconds = durationInSeconds.toInt(&ok); + if (ok) { + text.append(formatTime(timeoutSeconds)); + } } else { - this->message_->flags &= ~Message::Highlighted; + text.append(" has been permanently banned"); } + + if (reason.length() > 0) { + text.append(": \""); + text.append(parseTagString(reason)); + text.append("\""); + } + text.append("."); + + if (multipleTimes) { + text.append(" (multiple times)"); + } + + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::Timeout); + this->message().flags.set(MessageFlag::DoNotTriggerNotification); + this->message().timeoutUser = username; } -void MessageBuilder::appendTimestamp(const QTime &time) +MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) + : MessageBuilder() { - this->append(new TimestampElement(time)); + this->emplace(); + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::Timeout); + this->message().timeoutUser = action.target.name; + this->message().count = count; + + QString text; + + if (action.isBan()) { + 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 { + 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)); + } + } + + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + this->message().searchText = text; +} + +MessageBuilder::MessageBuilder(const UnbanAction &action) + : MessageBuilder() +{ + this->emplace(); + this->message().flags.set(MessageFlag::System); + this->message().flags.set(MessageFlag::Untimeout); + + this->message().timeoutUser = action.target.name; + + QString text; + + if (action.wasBan()) { + text = QString("%1 unbanned %2.") // + .arg(action.source.name) + .arg(action.target.name); + } else { + text = QString("%1 untimedout %2.") // + .arg(action.source.name) + .arg(action.target.name); + } + + this->emplace(text, MessageElementFlag::Text, + MessageColor::System); + this->message().searchText = text; +} + +Message *MessageBuilder::operator->() +{ + return this->message_.get(); +} + +Message &MessageBuilder::message() +{ + return *this->message_; +} + +MessagePtr MessageBuilder::release() +{ + return MessagePtr(this->message_.release()); +} + +void MessageBuilder::append(std::unique_ptr element) +{ + this->message().elements.push_back(std::move(element)); } QString MessageBuilder::matchLink(const QString &string) { LinkParser linkParser(string); - static QRegularExpression httpRegex("\\bhttps?://", QRegularExpression::CaseInsensitiveOption); - static QRegularExpression ftpRegex("\\bftps?://", QRegularExpression::CaseInsensitiveOption); + static QRegularExpression httpRegex( + "\\bhttps?://", QRegularExpression::CaseInsensitiveOption); + static QRegularExpression ftpRegex( + "\\bftps?://", QRegularExpression::CaseInsensitiveOption); + static QRegularExpression spotifyRegex( + "\\bspotify:", QRegularExpression::CaseInsensitiveOption); if (!linkParser.hasMatch()) { return QString(); @@ -56,10 +190,9 @@ QString MessageBuilder::matchLink(const QString &string) QString captured = linkParser.getCaptured(); - if (!captured.contains(httpRegex)) { - if (!captured.contains(ftpRegex)) { - captured.insert(0, "http://"); - } + if (!captured.contains(httpRegex) && !captured.contains(ftpRegex) && + !captured.contains(spotifyRegex)) { + captured.insert(0, "http://"); } return captured; diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index d2a617ec3..a2e78ec29 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -3,35 +3,60 @@ #include "messages/Message.hpp" #include - #include namespace chatterino { -struct MessageBuilder { +struct SystemMessageTag { +}; +struct TimeoutMessageTag { +}; +const SystemMessageTag systemMessage{}; +const TimeoutMessageTag timeoutMessage{}; + +MessagePtr makeSystemMessage(const QString &text); + +struct MessageParseArgs { + bool disablePingSounds = false; + bool isReceivedWhisper = false; + bool isSentWhisper = false; + bool trimSubscriberUsername = false; + bool isStaffOrBroadcaster = false; +}; + +class MessageBuilder +{ public: MessageBuilder(); + MessageBuilder(const QString &text); + MessageBuilder(SystemMessageTag, const QString &text); + MessageBuilder(TimeoutMessageTag, const QString &username, + const QString &durationInSeconds, const QString &reason, + bool multipleTimes); + MessageBuilder(const BanAction &action, uint32_t count = 1); + MessageBuilder(const UnbanAction &action); - MessagePtr getMessage(); + Message *operator->(); + Message &message(); + MessagePtr release(); - void setHighlight(bool value); - void append(MessageElement *element); - void appendTimestamp(); - void appendTimestamp(const QTime &time); + void append(std::unique_ptr element); QString matchLink(const QString &string); template T *emplace(Args &&... args) { - static_assert(std::is_base_of::value, "T must extend MessageElement"); + static_assert(std::is_base_of::value, + "T must extend MessageElement"); - T *element = new T(std::forward(args)...); - this->append(element); - return element; + auto unique = std::make_unique(std::forward(args)...); + auto pointer = unique.get(); + this->append(std::move(unique)); + return pointer; } -protected: - MessagePtr message_; +private: + std::unique_ptr message_; }; } // namespace chatterino diff --git a/src/messages/MessageContainer.cpp b/src/messages/MessageContainer.cpp new file mode 100644 index 000000000..2d47cb906 --- /dev/null +++ b/src/messages/MessageContainer.cpp @@ -0,0 +1,9 @@ +#include "MessageContainer.hpp" + +namespace chatterino { + +MessageContainer::MessageContainer() +{ +} + +} // namespace chatterino diff --git a/src/messages/MessageContainer.hpp b/src/messages/MessageContainer.hpp new file mode 100644 index 000000000..781135e60 --- /dev/null +++ b/src/messages/MessageContainer.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace chatterino { + +class MessageContainer +{ +public: + MessageContainer(); +}; + +} // namespace chatterino diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 5081f5e47..e31076107 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1,16 +1,16 @@ #include "messages/MessageElement.hpp" #include "Application.hpp" -#include "common/Emotemap.hpp" #include "controllers/moderationactions/ModerationActions.hpp" #include "debug/Benchmark.hpp" #include "messages/layouts/MessageLayoutContainer.hpp" #include "messages/layouts/MessageLayoutElement.hpp" #include "singletons/Settings.hpp" +#include "util/DebugCount.hpp" namespace chatterino { -MessageElement::MessageElement(Flags flags) +MessageElement::MessageElement(MessageElementFlags flags) : flags_(flags) { DebugCount::increase("message elements"); @@ -54,92 +54,100 @@ bool MessageElement::hasTrailingSpace() const return this->trailingSpace; } -MessageElement::Flags MessageElement::getFlags() const +MessageElementFlags MessageElement::getFlags() const { return this->flags_; } // IMAGE -ImageElement::ImageElement(Image *image, MessageElement::Flags flags) +ImageElement::ImageElement(ImagePtr image, MessageElementFlags flags) : MessageElement(flags) , image_(image) { - this->setTooltip(image->getTooltip()); + // this->setTooltip(image->getTooltip()); } -void ImageElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) +void ImageElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) { - if (flags & this->getFlags()) { - QSize size(this->image_->getScaledWidth() * container.getScale(), - this->image_->getScaledHeight() * container.getScale()); + if (flags.hasAny(this->getFlags())) { + auto size = QSize(this->image_->width() * container.getScale(), + this->image_->height() * container.getScale()); - container.addElement( - (new ImageLayoutElement(*this, this->image_, size))->setLink(this->getLink())); + container.addElement((new ImageLayoutElement(*this, this->image_, size)) + ->setLink(this->getLink())); } } // EMOTE -EmoteElement::EmoteElement(const EmoteData &data, MessageElement::Flags flags) +EmoteElement::EmoteElement(const EmotePtr &emote, MessageElementFlags flags) : MessageElement(flags) - , data(data) + , emote_(emote) { - if (data.isValid()) { - this->setTooltip(data.image1x->getTooltip()); - this->textElement_.reset( - new TextElement(data.image1x->getCopyString(), MessageElement::Misc)); - } + this->textElement_.reset( + new TextElement(emote->getCopyString(), MessageElementFlag::Misc)); + + this->setTooltip(emote->tooltip.string); } -void EmoteElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) +EmotePtr EmoteElement::getEmote() const { - if (flags & this->getFlags()) { - if (flags & MessageElement::EmoteImages) { - if (!this->data.isValid()) { - return; - } + return this->emote_; +} - Image *image = this->data.getImage(container.getScale()); +void EmoteElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (flags.hasAny(this->getFlags())) { + if (flags.has(MessageElementFlag::EmoteImages)) { + auto image = this->emote_->images.getImage(container.getScale()); + if (image->isEmpty()) return; - QSize size(int(container.getScale() * image->getScaledWidth()), - int(container.getScale() * image->getScaledHeight())); + 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((new ImageLayoutElement(*this, image, size)) + ->setLink(this->getLink())); } else { if (this->textElement_) { - this->textElement_->addToContainer(container, MessageElement::Misc); + this->textElement_->addToContainer(container, + MessageElementFlag::Misc); } } } } // TEXT -TextElement::TextElement(const QString &text, MessageElement::Flags flags, +TextElement::TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color, FontStyle style) : MessageElement(flags) , color_(color) , style_(style) { - for (QString word : text.split(' ')) { + for (const auto &word : text.split(' ')) { this->words_.push_back({word, -1}); // fourtf: add logic to store multiple spaces after message } } -void TextElement::addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) +void TextElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) { auto app = getApp(); - if (flags & this->getFlags()) { - QFontMetrics metrics = app->fonts->getFontMetrics(this->style_, container.getScale()); + if (flags.hasAny(this->getFlags())) { + QFontMetrics metrics = + app->fonts->getFontMetrics(this->style_, container.getScale()); for (Word &word : this->words_) { - auto getTextLayoutElement = [&](QString text, int width, bool trailingSpace) { + auto getTextLayoutElement = [&](QString text, int width, + bool trailingSpace) { QColor color = this->color_.getColor(*app->themes); app->themes->normalizeColor(color); - auto e = (new TextLayoutElement(*this, text, QSize(width, metrics.height()), color, - this->style_, container.getScale())) + auto e = (new TextLayoutElement( + *this, text, QSize(width, metrics.height()), + color, this->style_, container.getScale())) ->setLink(this->getLink()); e->setTrailingSpace(trailingSpace); return e; @@ -152,8 +160,8 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme // see if the text fits in the current line if (container.fitsInLine(word.width)) { - container.addElementNoLineBreak( - getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); + container.addElementNoLineBreak(getTextLayoutElement( + word.text, word.width, this->hasTrailingSpace())); continue; } @@ -162,8 +170,8 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme container.breakLine(); if (container.fitsInLine(word.width)) { - container.addElementNoLineBreak( - getTextLayoutElement(word.text, word.width, this->hasTrailingSpace())); + container.addElementNoLineBreak(getTextLayoutElement( + word.text, word.width, this->hasTrailingSpace())); continue; } } @@ -173,18 +181,16 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme int textLength = text.length(); int wordStart = 0; int width = metrics.width(text[0]); - int lastWidth = 0; for (int i = 1; i < textLength; i++) { int charWidth = metrics.width(text[i]); if (!container.fitsInLine(width + charWidth)) { - container.addElementNoLineBreak( - getTextLayoutElement(text.mid(wordStart, i - wordStart), width, false)); + container.addElementNoLineBreak(getTextLayoutElement( + text.mid(wordStart, i - wordStart), width, false)); container.breakLine(); wordStart = i; - lastWidth = width; width = 0; if (textLength > i + 2) { width += metrics.width(text[i]); @@ -196,10 +202,8 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme width += charWidth; } - UNUSED(lastWidth); // XXX: What should this be used for (if anything)? KKona - - container.addElement( - getTextLayoutElement(text.mid(wordStart), width, this->hasTrailingSpace())); + container.addElement(getTextLayoutElement( + text.mid(wordStart), width, this->hasTrailingSpace())); container.breakLine(); } } @@ -207,7 +211,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, MessageEleme // TIMESTAMP TimestampElement::TimestampElement(QTime time) - : MessageElement(MessageElement::Timestamp) + : MessageElement(MessageElementFlag::Timestamp) , time_(time) , element_(this->formatTime(time)) { @@ -215,9 +219,9 @@ TimestampElement::TimestampElement(QTime time) } void TimestampElement::addToContainer(MessageLayoutContainer &container, - MessageElement::Flags flags) + MessageElementFlags flags) { - if (flags & this->getFlags()) { + if (flags.hasAny(this->getFlags())) { auto app = getApp(); if (app->settings->timestampFormat != this->format_) { this->format_ = app->settings->timestampFormat.getValue(); @@ -234,29 +238,35 @@ TextElement *TimestampElement::formatTime(const QTime &time) QString format = locale.toString(time, getApp()->settings->timestampFormat); - return new TextElement(format, Flags::Timestamp, MessageColor::System, FontStyle::ChatMedium); + return new TextElement(format, MessageElementFlag::Timestamp, + MessageColor::System, FontStyle::ChatMedium); } // TWITCH MODERATION TwitchModerationElement::TwitchModerationElement() - : MessageElement(MessageElement::ModeratorTools) + : MessageElement(MessageElementFlag::ModeratorTools) { } void TwitchModerationElement::addToContainer(MessageLayoutContainer &container, - MessageElement::Flags flags) + MessageElementFlags flags) { - if (flags & MessageElement::ModeratorTools) { - QSize size(int(container.getScale() * 16), int(container.getScale() * 16)); + if (flags.has(MessageElementFlag::ModeratorTools)) { + QSize size(int(container.getScale() * 16), + int(container.getScale() * 16)); - for (const ModerationAction &m : getApp()->moderationActions->items.getVector()) { - if (m.isImage()) { - container.addElement((new ImageLayoutElement(*this, m.getImage(), size)) - ->setLink(Link(Link::UserAction, m.getAction()))); + for (const auto &action : + getApp()->moderationActions->items.getVector()) { + if (auto image = action.getImage()) { + container.addElement( + (new ImageLayoutElement(*this, image.get(), size)) + ->setLink(Link(Link::UserAction, action.getAction()))); } else { - container.addElement((new TextIconLayoutElement(*this, m.getLine1(), m.getLine2(), - container.getScale(), size)) - ->setLink(Link(Link::UserAction, m.getAction()))); + container.addElement( + (new TextIconLayoutElement(*this, action.getLine1(), + action.getLine2(), + container.getScale(), size)) + ->setLink(Link(Link::UserAction, action.getAction()))); } } } diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index d933c8dc3..79f222789 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -1,6 +1,7 @@ #pragma once -#include "common/Emotemap.hpp" +#include "common/FlagsEnum.hpp" +#include "messages/Emote.hpp" #include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" @@ -16,90 +17,101 @@ namespace chatterino { class Channel; -struct EmoteData; struct MessageLayoutContainer; +enum class MessageElementFlag { + None = 0, + Misc = (1 << 0), + Text = (1 << 1), + + Username = (1 << 2), + Timestamp = (1 << 3), + + TwitchEmoteImage = (1 << 4), + TwitchEmoteText = (1 << 5), + TwitchEmote = TwitchEmoteImage | TwitchEmoteText, + BttvEmoteImage = (1 << 6), + BttvEmoteText = (1 << 7), + BttvEmote = BttvEmoteImage | BttvEmoteText, + FfzEmoteImage = (1 << 10), + FfzEmoteText = (1 << 11), + FfzEmote = FfzEmoteImage | FfzEmoteText, + EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, + + BitsStatic = (1 << 12), + BitsAnimated = (1 << 13), + + // Slot 1: Twitch + // - Staff badge + // - Admin badge + // - Global Moderator badge + BadgeGlobalAuthority = (1 << 14), + + // Slot 2: Twitch + // - Moderator badge + // - Broadcaster badge + BadgeChannelAuthority = (1 << 15), + + // Slot 3: Twitch + // - Subscription badges + BadgeSubscription = (1 << 16), + + // Slot 4: Twitch + // - Turbo badge + // - Prime badge + // - Bit badges + // - Game badges + BadgeVanity = (1 << 17), + + // Slot 5: Chatterino + // - Chatterino developer badge + // - Chatterino donator badge + // - Chatterino top donator badge + BadgeChatterino = (1 << 18), + + // Rest of slots: ffz custom badge? bttv custom badge? mywaifu (puke) + // custom badge? + + Badges = BadgeGlobalAuthority | BadgeChannelAuthority | BadgeSubscription | + BadgeVanity | BadgeChatterino, + + ChannelName = (1 << 19), + + BitsAmount = (1 << 20), + + ModeratorTools = (1 << 21), + + EmojiImage = (1 << 23), + EmojiText = (1 << 24), + EmojiAll = EmojiImage | EmojiText, + + AlwaysShow = (1 << 25), + + // used in the ChannelView class to make the collapse buttons visible if + // needed + Collapsed = (1 << 26), + + // used for dynamic bold usernames + BoldUsername = (1 << 27), + NonBoldUsername = (1 << 28), + + // for links + LowercaseLink = (1 << 29), + OriginalLink = (1 << 30), + + Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage | + BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text | + AlwaysShow, +}; +using MessageElementFlags = FlagsEnum; + class MessageElement : boost::noncopyable { public: - enum Flags : uint32_t { - None = 0, - Misc = (1 << 0), - Text = (1 << 1), - - Username = (1 << 2), - Timestamp = (1 << 3), - - TwitchEmoteImage = (1 << 4), - TwitchEmoteText = (1 << 5), - TwitchEmote = TwitchEmoteImage | TwitchEmoteText, - BttvEmoteImage = (1 << 6), - BttvEmoteText = (1 << 7), - BttvEmote = BttvEmoteImage | BttvEmoteText, - FfzEmoteImage = (1 << 10), - FfzEmoteText = (1 << 11), - FfzEmote = FfzEmoteImage | FfzEmoteText, - EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, - - BitsStatic = (1 << 12), - BitsAnimated = (1 << 13), - - // Slot 1: Twitch - // - Staff badge - // - Admin badge - // - Global Moderator badge - BadgeGlobalAuthority = (1 << 14), - - // Slot 2: Twitch - // - Moderator badge - // - Broadcaster badge - BadgeChannelAuthority = (1 << 15), - - // Slot 3: Twitch - // - Subscription badges - BadgeSubscription = (1 << 16), - - // Slot 4: Twitch - // - Turbo badge - // - Prime badge - // - Bit badges - // - Game badges - BadgeVanity = (1 << 17), - - // Slot 5: Chatterino - // - Chatterino developer badge - // - Chatterino donator badge - // - Chatterino top donator badge - BadgeChatterino = (1 << 18), - - // Rest of slots: ffz custom badge? bttv custom badge? mywaifu (puke) custom badge? - - Badges = BadgeGlobalAuthority | BadgeChannelAuthority | BadgeSubscription | BadgeVanity | - BadgeChatterino, - - ChannelName = (1 << 19), - - BitsAmount = (1 << 20), - - ModeratorTools = (1 << 21), - - EmojiImage = (1 << 23), - EmojiText = (1 << 24), - EmojiAll = EmojiImage | EmojiText, - - AlwaysShow = (1 << 25), - - // used in the ChannelView class to make the collapse buttons visible if needed - Collapsed = (1 << 26), - - Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage | BttvEmoteImage | - TwitchEmoteImage | BitsAmount | Text | AlwaysShow, - }; - enum UpdateFlags : char { - Update_Text, - Update_Emotes, - Update_Images, + Update_Text = 1, + Update_Emotes = 2, + Update_Images = 4, Update_All = Update_Text | Update_Emotes | Update_Images }; @@ -111,42 +123,45 @@ public: const QString &getTooltip() const; const Link &getLink() const; bool hasTrailingSpace() const; - Flags getFlags() const; + MessageElementFlags getFlags() const; - virtual void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) = 0; + virtual void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) = 0; protected: - MessageElement(Flags flags); + MessageElement(MessageElementFlags flags); bool trailingSpace = true; private: Link link_; QString tooltip_; - Flags flags_; + MessageElementFlags flags_; }; // contains a simple image class ImageElement : public MessageElement { public: - ImageElement(Image *image, MessageElement::Flags flags); + ImageElement(ImagePtr image, MessageElementFlags flags); - void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; private: - Image *image_; + ImagePtr image_; }; // contains a text, it will split it into words class TextElement : public MessageElement { public: - TextElement(const QString &text, MessageElement::Flags flags, + TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color = MessageColor::Text, FontStyle style = FontStyle::ChatMedium); ~TextElement() override = default; - void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; private: MessageColor color_; @@ -165,15 +180,15 @@ private: class EmoteElement : public MessageElement { public: - EmoteElement(const EmoteData &data, MessageElement::Flags flags_); - ~EmoteElement() override = default; + EmoteElement(const EmotePtr &data, MessageElementFlags flags_); - void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags_) override; - - const EmoteData data; + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags_) override; + EmotePtr getEmote() const; private: std::unique_ptr textElement_; + EmotePtr emote_; }; // contains a text, formated depending on the preferences @@ -183,7 +198,8 @@ public: TimestampElement(QTime time_ = QTime::currentTime()); ~TimestampElement() override = default; - void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; TextElement *formatTime(const QTime &time); @@ -193,14 +209,15 @@ private: QString format_; }; -// adds all the custom moderation buttons, adds a variable amount of items depending on settings -// fourtf: implement +// adds all the custom moderation buttons, adds a variable amount of items +// depending on settings fourtf: implement class TwitchModerationElement : public MessageElement { public: TwitchModerationElement(); - void addToContainer(MessageLayoutContainer &container, MessageElement::Flags flags) override; + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) override; }; } // namespace chatterino diff --git a/src/messages/MessageParseArgs.hpp b/src/messages/MessageParseArgs.hpp index f3d732c4b..519262dc1 100644 --- a/src/messages/MessageParseArgs.hpp +++ b/src/messages/MessageParseArgs.hpp @@ -2,12 +2,5 @@ namespace chatterino { -struct MessageParseArgs { - bool disablePingSounds = false; - bool isReceivedWhisper = false; - bool isSentWhisper = false; - bool trimSubscriberUsername = false; - bool isStaffOrBroadcaster = false; -}; } // namespace chatterino diff --git a/src/messages/Selection.hpp b/src/messages/Selection.hpp index e5e6f3042..7cffebbec 100644 --- a/src/messages/Selection.hpp +++ b/src/messages/Selection.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace chatterino { @@ -23,13 +24,8 @@ struct SelectionItem { bool operator<(const SelectionItem &b) const { - if (this->messageIndex < b.messageIndex) { - return true; - } - if (this->messageIndex == b.messageIndex && this->charIndex < b.charIndex) { - return true; - } - return false; + return std::tie(this->messageIndex, this->charIndex) < + std::tie(b.messageIndex, b.charIndex); } bool operator>(const SelectionItem &b) const @@ -39,7 +35,8 @@ struct SelectionItem { bool operator==(const SelectionItem &b) const { - return this->messageIndex == b.messageIndex && this->charIndex == b.charIndex; + return this->messageIndex == b.messageIndex && + this->charIndex == b.charIndex; } bool operator!=(const SelectionItem &b) const @@ -74,7 +71,8 @@ struct Selection { bool isSingleMessage() const { - return this->selectionMin.messageIndex == this->selectionMax.messageIndex; + return this->selectionMin.messageIndex == + this->selectionMax.messageIndex; } }; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 259da3439..9847c1950 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -5,6 +5,7 @@ #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include "util/DebugCount.hpp" #include #include @@ -32,7 +33,7 @@ MessageLayout::~MessageLayout() DebugCount::decrease("message layout"); } -Message *MessageLayout::getMessage() +const Message *MessageLayout::getMessage() { return this->message_.get(); } @@ -45,7 +46,7 @@ int MessageLayout::getHeight() const // Layout // return true if redraw is required -bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) +bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) { // BenchmarkGuard benchmark("MessageLayout::layout()"); @@ -61,7 +62,7 @@ bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) // check if layout state changed if (this->layoutState_ != app->windows->getGeneration()) { layoutRequired = true; - this->flags |= RequiresBufferUpdate; + this->flags.set(MessageLayoutFlag::RequiresBufferUpdate); this->layoutState_ = app->windows->getGeneration(); } @@ -70,8 +71,8 @@ bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) this->currentWordFlags_ = flags; // app->settings->getWordTypeMask(); // check if layout was requested manually - layoutRequired |= bool(this->flags & RequiresLayout); - this->flags &= decltype(RequiresLayout)(~RequiresLayout); + layoutRequired |= this->flags.has(MessageLayoutFlag::RequiresLayout); + this->flags.unset(MessageLayoutFlag::RequiresLayout); // check if dpi changed layoutRequired |= this->scale_ != scale; @@ -91,19 +92,20 @@ bool MessageLayout::layout(int width, float scale, MessageElement::Flags flags) return true; } -void MessageLayout::actuallyLayout(int width, MessageElement::Flags _flags) +void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags) { - auto messageFlags = this->message_->flags.value; + auto messageFlags = this->message_->flags; - if (this->flags & MessageLayout::Expanded || - (_flags & MessageElement::ModeratorTools && - !(this->message_->flags & Message::MessageFlags::Disabled))) { - messageFlags = Message::MessageFlags(messageFlags & ~Message::MessageFlags::Collapsed); + if (this->flags.has(MessageLayoutFlag::Expanded) || + (_flags.has(MessageElementFlag::ModeratorTools) && + !this->message_->flags.has(MessageFlag::Disabled))) // + { + messageFlags.unset(MessageFlag::Collapsed); } this->container_.begin(width, this->scale_, messageFlags); - for (const std::unique_ptr &element : this->message_->getElements()) { + for (const auto &element : this->message_->elements) { element->addToContainer(this->container_, _flags); } @@ -115,15 +117,16 @@ void MessageLayout::actuallyLayout(int width, MessageElement::Flags _flags) this->height_ = this->container_.getHeight(); // collapsed state - this->flags &= ~Flags::Collapsed; + this->flags.unset(MessageLayoutFlag::Collapsed); if (this->container_.isCollapsed()) { - this->flags |= Flags::Collapsed; + this->flags.set(MessageLayoutFlag::Collapsed); } } // Painting void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, - Selection &selection, bool isLastReadMessage, bool isWindowFocused) + Selection &selection, bool isLastReadMessage, + bool isWindowFocused) { auto app = getApp(); QPixmap *pixmap = this->buffer_.get(); @@ -132,7 +135,8 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, if (!pixmap) { #ifdef Q_OS_MACOS pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()), - int(container_.getHeight() * painter.device()->devicePixelRatioF())); + int(container_.getHeight() * + painter.device()->devicePixelRatioF())); pixmap->setDevicePixelRatio(painter.device()->devicePixelRatioF()); #else pixmap = new QPixmap(width, std::max(16, this->container_.getHeight())); @@ -149,14 +153,16 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, // draw on buffer painter.drawPixmap(0, y, *pixmap); - // painter.drawPixmap(0, y, this->container.width, this->container.getHeight(), *pixmap); + // painter.drawPixmap(0, y, this->container.width, + // this->container.getHeight(), *pixmap); // draw gif emotes this->container_.paintAnimatedElements(painter, y); // draw disabled - if (this->message_->flags.HasFlag(Message::Disabled)) { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), app->themes->messages.disabled); + if (this->message_->flags.has(MessageFlag::Disabled)) { + painter.fillRect(0, y, pixmap->width(), pixmap->height(), + app->themes->messages.disabled); } // draw selection @@ -172,19 +178,23 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, // draw last read message line if (isLastReadMessage) { - QColor color = isWindowFocused ? app->themes->tabs.selected.backgrounds.regular.color() - : app->themes->tabs.selected.backgrounds.unfocused.color(); + QColor color = + isWindowFocused + ? app->themes->tabs.selected.backgrounds.regular.color() + : app->themes->tabs.selected.backgrounds.unfocused.color(); - QBrush brush(color, - static_cast(app->settings->lastMessagePattern.getValue())); + QBrush brush(color, static_cast( + app->settings->lastMessagePattern.getValue())); - painter.fillRect(0, y + this->container_.getHeight() - 1, pixmap->width(), 1, brush); + painter.fillRect(0, y + this->container_.getHeight() - 1, + pixmap->width(), 1, brush); } this->bufferValid_ = true; } -void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, Selection & /*selection*/) +void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, + Selection & /*selection*/) { auto app = getApp(); @@ -194,12 +204,12 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, Selectio // draw background QColor backgroundColor; - if (this->message_->flags & Message::Highlighted) { + if (this->message_->flags.has(MessageFlag::Highlighted)) { backgroundColor = app->themes->messages.backgrounds.highlighted; - } else if (this->message_->flags & Message::Subscription) { + } else if (this->message_->flags.has(MessageFlag::Subscription)) { backgroundColor = app->themes->messages.backgrounds.subscription; } else if (app->settings->alternateMessageBackground.getValue() && - this->flags & MessageLayout::AlternateBackground) { + this->flags.has(MessageLayoutFlag::AlternateBackground)) { backgroundColor = app->themes->messages.backgrounds.alternate; } else { backgroundColor = app->themes->messages.backgrounds.regular; @@ -212,8 +222,8 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/, Selectio #ifdef FOURTF // debug painter.setPen(QColor(255, 0, 0)); - painter.drawRect(buffer->rect().x(), buffer->rect().y(), buffer->rect().width() - 1, - buffer->rect().height() - 1); + painter.drawRect(buffer->rect().x(), buffer->rect().y(), + buffer->rect().width() - 1, buffer->rect().height() - 1); QTextOption option; option.setAlignment(Qt::AlignRight | Qt::AlignTop); diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp index f7354a3ae..0f0d5cab3 100644 --- a/src/messages/layouts/MessageLayout.hpp +++ b/src/messages/layouts/MessageLayout.hpp @@ -14,34 +14,33 @@ namespace chatterino { +enum class MessageLayoutFlag : uint8_t { + RequiresBufferUpdate = 1 << 1, + RequiresLayout = 1 << 2, + AlternateBackground = 1 << 3, + Collapsed = 1 << 4, + Expanded = 1 << 5, +}; +using MessageLayoutFlags = FlagsEnum; + class MessageLayout : boost::noncopyable { public: - enum Flags : uint8_t { - RequiresBufferUpdate = 1 << 1, - RequiresLayout = 1 << 2, - AlternateBackground = 1 << 3, - Collapsed = 1 << 4, - Expanded = 1 << 5, - }; - MessageLayout(MessagePtr message_); ~MessageLayout(); - Message *getMessage(); + const Message *getMessage(); - // Height int getHeight() const; - // Flags - FlagsEnum flags; + MessageLayoutFlags flags; - // Layout - bool layout(int width, float scale_, MessageElement::Flags flags); + bool layout(int width, float scale_, MessageElementFlags flags); // Painting - void paint(QPainter &painter, int width, int y, int messageIndex, Selection &selection, - bool isLastReadMessage, bool isWindowFocused); + void paint(QPainter &painter, int width, int y, int messageIndex, + Selection &selection, bool isLastReadMessage, + bool isWindowFocused); void invalidateBuffer(); void deleteBuffer(); void deleteCache(); @@ -69,12 +68,12 @@ private: float scale_ = -1; unsigned int bufferUpdatedCount_ = 0; - MessageElement::Flags currentWordFlags_ = MessageElement::None; + MessageElementFlags currentWordFlags_; int collapsedHeight_ = 32; // methods - void actuallyLayout(int width, MessageElement::Flags flags); + void actuallyLayout(int width, MessageElementFlags flags); void updateBuffer(QPixmap *pixmap, int messageIndex, Selection &selection); }; diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index 19ecfae98..699cfc604 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -9,7 +9,8 @@ #include #define COMPACT_EMOTES_OFFSET 6 -#define MAX_UNCOLLAPSED_LINES (getApp()->settings->collpseMessagesMinLines.getValue()) +#define MAX_UNCOLLAPSED_LINES \ + (getApp()->settings->collpseMessagesMinLines.getValue()) namespace chatterino { @@ -29,13 +30,14 @@ float MessageLayoutContainer::getScale() const } // methods -void MessageLayoutContainer::begin(int width, float scale, Message::MessageFlags flags) +void MessageLayoutContainer::begin(int width, float scale, MessageFlags flags) { this->clear(); this->width_ = width; this->scale_ = scale; this->flags_ = flags; - auto mediumFontMetrics = getApp()->fonts->getFontMetrics(FontStyle::ChatMedium, scale); + auto mediumFontMetrics = + getApp()->fonts->getFontMetrics(FontStyle::ChatMedium, scale); this->textLineHeight_ = mediumFontMetrics.height(); this->spaceWidth_ = mediumFontMetrics.width(' '); this->dotdotdotWidth_ = mediumFontMetrics.width("..."); @@ -66,7 +68,8 @@ void MessageLayoutContainer::addElement(MessageLayoutElement *element) this->_addElement(element); } -void MessageLayoutContainer::addElementNoLineBreak(MessageLayoutElement *element) +void MessageLayoutContainer::addElementNoLineBreak( + MessageLayoutElement *element) { this->_addElement(element); } @@ -76,7 +79,8 @@ bool MessageLayoutContainer::canAddElements() return this->canAddMessages_; } -void MessageLayoutContainer::_addElement(MessageLayoutElement *element, bool forceAdd) +void MessageLayoutContainer::_addElement(MessageLayoutElement *element, + bool forceAdd) { if (!this->canAddElements() && !forceAdd) { delete element; @@ -91,8 +95,9 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element, bool for int newLineHeight = element->getRect().height(); // compact emote offset - bool isCompactEmote = !(this->flags_ & Message::DisableCompactEmotes) && - element->getCreator().getFlags() & MessageElement::EmoteImages; + bool isCompactEmote = + !this->flags_.has(MessageFlag::DisableCompactEmotes) && + element->getCreator().getFlags().has(MessageElementFlag::EmoteImages); if (isCompactEmote) { newLineHeight -= COMPACT_EMOTES_OFFSET * this->scale_; @@ -102,7 +107,8 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element, bool for this->lineHeight_ = std::max(this->lineHeight_, newLineHeight); // set move element - element->setPosition(QPoint(this->currentX_, this->currentY_ - element->getRect().height())); + element->setPosition( + QPoint(this->currentX_, this->currentY_ - element->getRect().height())); // add element this->elements_.push_back(std::unique_ptr(element)); @@ -119,36 +125,46 @@ void MessageLayoutContainer::breakLine() { int xOffset = 0; - if (this->flags_ & Message::Centered && this->elements_.size() > 0) { - xOffset = (width_ - this->elements_.at(this->elements_.size() - 1)->getRect().right()) / 2; + if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0) { + xOffset = (width_ - this->elements_.at(0)->getRect().left() - + this->elements_.at(this->elements_.size() - 1) + ->getRect() + .right()) / + 2; } for (size_t i = lineStart_; i < this->elements_.size(); i++) { MessageLayoutElement *element = this->elements_.at(i).get(); - bool isCompactEmote = !(this->flags_ & Message::DisableCompactEmotes) && - element->getCreator().getFlags() & MessageElement::EmoteImages; + bool isCompactEmote = + !this->flags_.has(MessageFlag::DisableCompactEmotes) && + element->getCreator().getFlags().has( + MessageElementFlag::EmoteImages); int yExtra = 0; if (isCompactEmote) { yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; } - // if (element->getCreator().getFlags() & MessageElement::Badges) { + // if (element->getCreator().getFlags() & + // MessageElementFlag::Badges) + // { if (element->getRect().height() < this->textLineHeight_) { yExtra -= (this->textLineHeight_ - element->getRect().height()) / 2; } - element->setPosition(QPoint(element->getRect().x() + xOffset + this->margin.left, - element->getRect().y() + this->lineHeight_ + yExtra)); + element->setPosition( + QPoint(element->getRect().x() + xOffset + this->margin.left, + element->getRect().y() + this->lineHeight_ + yExtra)); } if (this->lines_.size() != 0) { this->lines_.back().endIndex = this->lineStart_; this->lines_.back().endCharIndex = this->charIndex_; } - this->lines_.push_back({(int)lineStart_, 0, this->charIndex_, 0, - QRect(-100000, this->currentY_, 200000, lineHeight_)}); + this->lines_.push_back( + {(int)lineStart_, 0, this->charIndex_, 0, + QRect(-100000, this->currentY_, 200000, lineHeight_)}); for (int i = this->lineStart_; i < this->elements_.size(); i++) { this->charIndex_ += this->elements_[i]->getSelectionIndexCount(); @@ -178,17 +194,20 @@ bool MessageLayoutContainer::fitsInLine(int _width) { return this->currentX_ + _width <= (this->width_ - this->margin.left - this->margin.right - - (this->line_ + 1 == MAX_UNCOLLAPSED_LINES ? this->dotdotdotWidth_ : 0)); + (this->line_ + 1 == MAX_UNCOLLAPSED_LINES ? this->dotdotdotWidth_ + : 0)); } void MessageLayoutContainer::end() { if (!this->canAddElements()) { - static TextElement dotdotdot("...", MessageElement::Collapsed, MessageColor::Link); + static TextElement dotdotdot("...", MessageElementFlag::Collapsed, + MessageColor::Link); static QString dotdotdotText("..."); auto *element = new TextLayoutElement( - dotdotdot, dotdotdotText, QSize(this->dotdotdotWidth_, this->textLineHeight_), + dotdotdot, dotdotdotText, + QSize(this->dotdotdotWidth_, this->textLineHeight_), QColor("#00D80A"), FontStyle::ChatMediumBold, this->scale_); // getApp()->themes->messages.textColors.system @@ -213,7 +232,7 @@ void MessageLayoutContainer::end() bool MessageLayoutContainer::canCollapse() { return getApp()->settings->collpseMessagesMinLines.getValue() > 0 && - this->flags_ & Message::MessageFlags::Collapsed; + this->flags_.has(MessageFlag::Collapsed); } bool MessageLayoutContainer::isCollapsed() @@ -235,7 +254,8 @@ MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point) // painting void MessageLayoutContainer::paintElements(QPainter &painter) { - for (const std::unique_ptr &element : this->elements_) { + for (const std::unique_ptr &element : + this->elements_) { #ifdef FOURTF painter.setPen(QColor(0, 255, 0)); painter.drawRect(element->getRect()); @@ -245,9 +265,11 @@ void MessageLayoutContainer::paintElements(QPainter &painter) } } -void MessageLayoutContainer::paintAnimatedElements(QPainter &painter, int yOffset) +void MessageLayoutContainer::paintAnimatedElements(QPainter &painter, + int yOffset) { - for (const std::unique_ptr &element : this->elements_) { + for (const std::unique_ptr &element : + this->elements_) { element->paintAnimated(painter, yOffset); } } @@ -273,7 +295,8 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, rect.setTop(std::max(0, rect.top()) + yOffset); rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight(this->elements_[line.endIndex - 1]->getRect().right()); + rect.setRight( + this->elements_[line.endIndex - 1]->getRect().right()); painter.fillRect(rect, selectionColor); } @@ -302,16 +325,19 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, int c = this->elements_[i]->getSelectionIndexCount(); if (index + c > selection.selectionMin.charIndex) { - x = this->elements_[i]->getXFromIndex(selection.selectionMin.charIndex - index); + x = this->elements_[i]->getXFromIndex( + selection.selectionMin.charIndex - index); // ends in same line if (selection.selectionMax.messageIndex == messageIndex && - line.endCharIndex > /*=*/selection.selectionMax.charIndex) // + line.endCharIndex > + /*=*/selection.selectionMax.charIndex) // { returnAfter = true; index = line.startCharIndex; for (int i = line.startIndex; i < line.endIndex; i++) { - int c = this->elements_[i]->getSelectionIndexCount(); + int c = + this->elements_[i]->getSelectionIndexCount(); if (index + c > selection.selectionMax.charIndex) { r = this->elements_[i]->getXFromIndex( @@ -330,9 +356,15 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, QRect rect = line.rect; rect.setTop(std::max(0, rect.top()) + yOffset); - rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight(this->elements_[line.endIndex - 1]->getRect().right()); + rect.setBottom( + std::min(this->height_, rect.bottom()) + + yOffset); + rect.setLeft(this->elements_[line.startIndex] + ->getRect() + .left()); + rect.setRight(this->elements_[line.endIndex - 1] + ->getRect() + .right()); painter.fillRect(rect, selectionColor); } @@ -378,7 +410,8 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, rect.setTop(std::max(0, rect.top()) + yOffset); rect.setBottom(std::min(this->height_, rect.bottom()) + yOffset); rect.setLeft(this->elements_[line.startIndex]->getRect().left()); - rect.setRight(this->elements_[line.endIndex - 1]->getRect().right()); + rect.setRight( + this->elements_[line.endIndex - 1]->getRect().right()); painter.fillRect(rect, selectionColor); continue; @@ -390,7 +423,8 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, int c = this->elements_[i]->getSelectionIndexCount(); if (index + c > selection.selectionMax.charIndex) { - r = this->elements_[i]->getXFromIndex(selection.selectionMax.charIndex - index); + r = this->elements_[i]->getXFromIndex( + selection.selectionMax.charIndex - index); break; } @@ -424,11 +458,13 @@ int MessageLayoutContainer::getSelectionIndex(QPoint point) } } - int lineStart = line == this->lines_.end() ? this->lines_.back().startIndex : line->startIndex; + int lineStart = line == this->lines_.end() ? this->lines_.back().startIndex + : line->startIndex; if (line != this->lines_.end()) { line++; } - int lineEnd = line == this->lines_.end() ? this->elements_.size() : line->startIndex; + int lineEnd = + line == this->lines_.end() ? this->elements_.size() : line->startIndex; int index = 0; diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index 3e00effce..f52f181e6 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -51,7 +51,7 @@ struct MessageLayoutContainer { float getScale() const; // methods - void begin(int width_, float scale_, Message::MessageFlags flags_); + void begin(int width_, float scale_, MessageFlags flags_); void end(); void clear(); @@ -66,7 +66,8 @@ struct MessageLayoutContainer { // painting void paintElements(QPainter &painter); void paintAnimatedElements(QPainter &painter, int yOffset); - void paintSelection(QPainter &painter, int messageIndex, Selection &selection, int yOffset); + void paintSelection(QPainter &painter, int messageIndex, + Selection &selection, int yOffset); // selection int getSelectionIndex(QPoint point); @@ -91,7 +92,7 @@ private: // variables float scale_ = 1.f; int width_ = 0; - Message::MessageFlags flags_ = Message::MessageFlags::None; + MessageFlags flags_ = MessageFlag::None; int line_ = 0; int height_ = 0; int currentX_ = 0; diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index c71858eec..e8d333e2b 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -14,7 +14,8 @@ const QRect &MessageLayoutElement::getRect() const return this->rect_; } -MessageLayoutElement::MessageLayoutElement(MessageElement &creator, const QSize &size) +MessageLayoutElement::MessageLayoutElement(MessageElement &creator, + const QSize &size) : creator_(creator) { this->rect_.setSize(size); @@ -63,16 +64,19 @@ const Link &MessageLayoutElement::getLink() const // IMAGE // -ImageLayoutElement::ImageLayoutElement(MessageElement &_creator, Image *_image, const QSize &_size) - : MessageLayoutElement(_creator, _size) - , image(_image) +ImageLayoutElement::ImageLayoutElement(MessageElement &creator, ImagePtr image, + const QSize &size) + : MessageLayoutElement(creator, size) + , image_(image) { - this->trailingSpace = _creator.hasTrailingSpace(); + this->trailingSpace = creator.hasTrailingSpace(); } -void ImageLayoutElement::addCopyTextToString(QString &str, int from, int to) const +void ImageLayoutElement::addCopyTextToString(QString &str, int from, + int to) const { - str += this->image->getCopyString(); + // str += this->image_->getCopyString(); + str += "not implemented"; if (this->hasTrailingSpace()) { str += " "; @@ -86,13 +90,12 @@ int ImageLayoutElement::getSelectionIndexCount() void ImageLayoutElement::paint(QPainter &painter) { - if (this->image == nullptr) { + if (this->image_ == nullptr) { return; } - const QPixmap *pixmap = this->image->getPixmap(); - - if (pixmap != nullptr && !this->image->isAnimated()) { + auto pixmap = this->image_->pixmap(); + if (pixmap && !this->image_->animated()) { // fourtf: make it use qreal values painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF()); } @@ -100,19 +103,15 @@ void ImageLayoutElement::paint(QPainter &painter) void ImageLayoutElement::paintAnimated(QPainter &painter, int yOffset) { - if (this->image == nullptr) { + if (this->image_ == nullptr) { return; } - if (this->image->isAnimated()) { - // qDebug() << this->image->getUrl(); - auto pixmap = this->image->getPixmap(); - - if (pixmap != nullptr) { - // fourtf: make it use qreal values - QRect _rect = this->getRect(); - _rect.moveTop(_rect.y() + yOffset); - painter.drawPixmap(QRectF(_rect), *pixmap, QRectF()); + if (this->image_->animated()) { + if (auto pixmap = this->image_->pixmap()) { + auto rect = this->getRect(); + rect.moveTop(rect.y() + yOffset); + painter.drawPixmap(QRectF(rect), *pixmap, QRectF()); } } } @@ -138,8 +137,9 @@ int ImageLayoutElement::getXFromIndex(int index) // TEXT // -TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text, const QSize &_size, - QColor _color, FontStyle _style, float _scale) +TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text, + const QSize &_size, QColor _color, + FontStyle _style, float _scale) : MessageLayoutElement(_creator, _size) , text(_text) , color(_color) @@ -148,7 +148,8 @@ TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text, c { } -void TextLayoutElement::addCopyTextToString(QString &str, int from, int to) const +void TextLayoutElement::addCopyTextToString(QString &str, int from, + int to) const { str += this->text.mid(from, to - from); @@ -170,8 +171,9 @@ void TextLayoutElement::paint(QPainter &painter) painter.setFont(app->fonts->getFont(this->style, this->scale)); - painter.drawText(QRectF(this->getRect().x(), this->getRect().y(), 10000, 10000), this->text, - QTextOption(Qt::AlignLeft | Qt::AlignTop)); + painter.drawText( + QRectF(this->getRect().x(), this->getRect().y(), 10000, 10000), + this->text, QTextOption(Qt::AlignLeft | Qt::AlignTop)); } void TextLayoutElement::paintAnimated(QPainter &, int) @@ -223,8 +225,10 @@ int TextLayoutElement::getXFromIndex(int index) } // TEXT ICON -TextIconLayoutElement::TextIconLayoutElement(MessageElement &creator, const QString &_line1, - const QString &_line2, float _scale, const QSize &size) +TextIconLayoutElement::TextIconLayoutElement(MessageElement &creator, + const QString &_line1, + const QString &_line2, + float _scale, const QSize &size) : MessageLayoutElement(creator, size) , scale(_scale) , line1(_line1) @@ -232,7 +236,8 @@ TextIconLayoutElement::TextIconLayoutElement(MessageElement &creator, const QStr { } -void TextIconLayoutElement::addCopyTextToString(QString &str, int from, int to) const +void TextIconLayoutElement::addCopyTextToString(QString &str, int from, + int to) const { } @@ -258,11 +263,12 @@ void TextIconLayoutElement::paint(QPainter &painter) painter.drawText(_rect, this->line1, option); } else { painter.drawText( - QPoint(this->getRect().x(), this->getRect().y() + this->getRect().height() / 2), + QPoint(this->getRect().x(), + this->getRect().y() + this->getRect().height() / 2), this->line1); - painter.drawText( - QPoint(this->getRect().x(), this->getRect().y() + this->getRect().height()), - this->line2); + painter.drawText(QPoint(this->getRect().x(), + this->getRect().y() + this->getRect().height()), + this->line2); } } diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 3d14b0f32..44b4c9316 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -7,6 +7,7 @@ #include #include +#include "messages/Image.hpp" #include "messages/Link.hpp" #include "messages/MessageColor.hpp" #include "singletons/Fonts.hpp" @@ -15,7 +16,6 @@ class QPainter; namespace chatterino { class MessageElement; -class Image; class MessageLayoutElement : boost::noncopyable { @@ -31,7 +31,8 @@ public: MessageLayoutElement *setTrailingSpace(bool value); MessageLayoutElement *setLink(const Link &link_); - virtual void addCopyTextToString(QString &str, int from = 0, int to = INT_MAX) const = 0; + virtual void addCopyTextToString(QString &str, int from = 0, + int to = INT_MAX) const = 0; virtual int getSelectionIndexCount() = 0; virtual void paint(QPainter &painter) = 0; virtual void paintAnimated(QPainter &painter, int yOffset) = 0; @@ -52,10 +53,12 @@ private: class ImageLayoutElement : public MessageLayoutElement { public: - ImageLayoutElement(MessageElement &creator_, Image *image, const QSize &size); + ImageLayoutElement(MessageElement &creator, ImagePtr image, + const QSize &size); protected: - void addCopyTextToString(QString &str, int from = 0, int to = INT_MAX) const override; + void addCopyTextToString(QString &str, int from = 0, + int to = INT_MAX) const override; int getSelectionIndexCount() override; void paint(QPainter &painter) override; void paintAnimated(QPainter &painter, int yOffset) override; @@ -63,18 +66,20 @@ protected: int getXFromIndex(int index) override; private: - Image *image; + ImagePtr image_; }; // TEXT class TextLayoutElement : public MessageLayoutElement { public: - TextLayoutElement(MessageElement &creator_, QString &text, const QSize &size, QColor color, - FontStyle style, float scale); + TextLayoutElement(MessageElement &creator_, QString &text, + const QSize &size, QColor color, FontStyle style, + float scale); protected: - void addCopyTextToString(QString &str, int from = 0, int to = INT_MAX) const override; + void addCopyTextToString(QString &str, int from = 0, + int to = INT_MAX) const override; int getSelectionIndexCount() override; void paint(QPainter &painter) override; void paintAnimated(QPainter &painter, int yOffset) override; @@ -93,11 +98,12 @@ private: class TextIconLayoutElement : public MessageLayoutElement { public: - TextIconLayoutElement(MessageElement &creator_, const QString &line1, const QString &line2, - float scale, const QSize &size); + TextIconLayoutElement(MessageElement &creator_, const QString &line1, + const QString &line2, float scale, const QSize &size); protected: - void addCopyTextToString(QString &str, int from = 0, int to = INT_MAX) const override; + void addCopyTextToString(QString &str, int from = 0, + int to = INT_MAX) const override; int getSelectionIndexCount() override; void paint(QPainter &painter) override; void paintAnimated(QPainter &painter, int yOffset) override; diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 1c1f757c0..1d9115c09 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -1,126 +1,151 @@ #include "providers/bttv/BttvEmotes.hpp" -#include "common/UrlFetch.hpp" +#include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" +#include "messages/ImageSet.hpp" +#include "providers/twitch/TwitchChannel.hpp" + +#include +#include namespace chatterino { - namespace { - -QString getEmoteLink(QString urlTemplate, const QString &id, const QString &emoteScale) +Url getEmoteLink(QString urlTemplate, const EmoteId &id, + const QString &emoteScale) { urlTemplate.detach(); - return urlTemplate.replace("{{id}}", id).replace("{{image}}", emoteScale); + return {urlTemplate.replace("{{id}}", id.string) + .replace("{{image}}", emoteScale)}; } +std::pair parseGlobalEmotes(const QJsonObject &jsonRoot, + const EmoteMap ¤tEmotes) +{ + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = qS("https:") + jsonRoot.value("urlTemplate").toString(); + for (auto jsonEmote : jsonEmotes) { + auto id = EmoteId{jsonEmote.toObject().value("id").toString()}; + auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; + + auto emote = Emote( + {name, + ImageSet{ + Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Global Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); + + emotes[name] = cachedOrMakeEmotePtr(std::move(emote), currentEmotes); + } + + return {Success, std::move(emotes)}; +} +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} +std::pair parseChannelEmotes(const QJsonObject &jsonRoot) +{ + auto emotes = EmoteMap(); + auto jsonEmotes = jsonRoot.value("emotes").toArray(); + auto urlTemplate = "https:" + jsonRoot.value("urlTemplate").toString(); + + for (auto jsonEmote_ : jsonEmotes) { + auto jsonEmote = jsonEmote_.toObject(); + + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto name = EmoteName{jsonEmote.value("code").toString()}; + // emoteObject.value("imageType").toString(); + + auto emote = Emote( + {name, + ImageSet{ + Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1), + Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5), + Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)}, + Tooltip{name.string + "
Channel Bttv Emote"}, + Url{"https://manage.betterttv.net/emotes/" + id.string}}); + + emotes[name] = cachedOrMake(std::move(emote), id); + } + + return {Success, std::move(emotes)}; +} } // namespace -void BTTVEmotes::loadGlobalEmotes() +// +// BttvEmotes +// +BttvEmotes::BttvEmotes() + : global_(std::make_shared()) { - QString url("https://api.betterttv.net/2/emotes"); +} + +std::shared_ptr BttvEmotes::global() const +{ + return this->global_.get(); +} + +boost::optional BttvEmotes::global(const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +void BttvEmotes::loadGlobal() +{ + auto request = NetworkRequest(QString(globalEmoteApiUrl)); - NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(30000); - request.setUseQuickLoadCache(true); - request.onSuccess([this](auto result) { - auto root = result.parseJson(); - auto emotes = root.value("emotes").toArray(); - QString urlTemplate = "https:" + root.value("urlTemplate").toString(); - - std::vector codes; - for (const QJsonValue &emote : emotes) { - QString id = emote.toObject().value("id").toString(); - QString code = emote.toObject().value("code").toString(); - - EmoteData emoteData; - emoteData.image1x = new Image(getEmoteLink(urlTemplate, id, "1x"), 1, code, - code + "
Global BTTV Emote"); - emoteData.image2x = new Image(getEmoteLink(urlTemplate, id, "2x"), 0.5, code, - code + "
Global BTTV Emote"); - emoteData.image3x = new Image(getEmoteLink(urlTemplate, id, "3x"), 0.25, code, - code + "
Global BTTV Emote"); - emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; - - this->globalEmotes.insert(code, emoteData); - codes.push_back(code); - } - - this->globalEmoteCodes = codes; - - return true; + request.onSuccess([this](auto result) -> Outcome { + auto emotes = this->global_.get(); + auto pair = parseGlobalEmotes(result.parseJson(), *emotes); + if (pair.first) + this->global_.set( + std::make_shared(std::move(pair.second))); + return pair.first; }); request.execute(); } -void BTTVEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) +void BttvEmotes::loadChannel(const QString &channelName, + std::function callback) { - printf("[BTTVEmotes] Reload BTTV Channel Emotes for channel %s\n", qPrintable(channelName)); + auto request = + NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelName); - QString url("https://api.betterttv.net/2/channels/" + channelName); - - Log("Request bttv channel emotes for {}", channelName); - - NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(3000); - request.setUseQuickLoadCache(true); - request.onSuccess([this, channelName, _map](auto result) { - auto rootNode = result.parseJson(); - auto map = _map.lock(); - if (_map.expired()) { - return false; - } - - map->clear(); - - auto emotesNode = rootNode.value("emotes").toArray(); - - QString linkTemplate = "https:" + rootNode.value("urlTemplate").toString(); - - std::vector codes; - for (const QJsonValue &emoteNode : emotesNode) { - QJsonObject emoteObject = emoteNode.toObject(); - - QString id = emoteObject.value("id").toString(); - QString code = emoteObject.value("code").toString(); - // emoteObject.value("imageType").toString(); - - auto emote = this->channelEmoteCache_.getOrAdd(id, [&] { - EmoteData emoteData; - QString link = linkTemplate; - link.detach(); - emoteData.image1x = new Image(link.replace("{{id}}", id).replace("{{image}}", "1x"), - 1, code, code + "
Channel BTTV Emote"); - link = linkTemplate; - link.detach(); - emoteData.image2x = new Image(link.replace("{{id}}", id).replace("{{image}}", "2x"), - 0.5, code, code + "
Channel BTTV Emote"); - link = linkTemplate; - link.detach(); - emoteData.image3x = new Image(link.replace("{{id}}", id).replace("{{image}}", "3x"), - 0.25, code, code + "
Channel BTTV Emote"); - emoteData.pageLink = "https://manage.betterttv.net/emotes/" + id; - - return emoteData; - }); - - this->channelEmotes.insert(code, emote); - map->insert(code, emote); - codes.push_back(code); - } - - this->channelEmoteCodes[channelName] = codes; - - return true; + request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { + auto pair = parseChannelEmotes(result.parseJson()); + if (pair.first) callback(std::move(pair.second)); + return pair.first; }); request.execute(); } +static Url getEmoteLink(QString urlTemplate, const EmoteId &id, + const QString &emoteScale) +{ + urlTemplate.detach(); + + return {urlTemplate.replace("{{id}}", id.string) + .replace("{{image}}", emoteScale)}; +} + } // namespace chatterino diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index 134e5a157..7392cd6bc 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -1,27 +1,29 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" -#include "util/ConcurrentMap.hpp" - -#include +#include +#include "common/Atomic.hpp" +#include "messages/Emote.hpp" namespace chatterino { -class BTTVEmotes +class BttvEmotes final { + static constexpr const char *globalEmoteApiUrl = + "https://api.betterttv.net/2/emotes"; + static constexpr const char *bttvChannelEmoteApiUrl = + "https://api.betterttv.net/2/channels/"; + public: - EmoteMap globalEmotes; - SimpleSignalVector globalEmoteCodes; + BttvEmotes(); - EmoteMap channelEmotes; - std::map> channelEmoteCodes; - - void loadGlobalEmotes(); - void loadChannelEmotes(const QString &channelName, std::weak_ptr channelEmoteMap); + std::shared_ptr global() const; + boost::optional global(const EmoteName &name) const; + void loadGlobal(); + static void loadChannel(const QString &channelName, + std::function callback); private: - EmoteMap channelEmoteCache_; + Atomic> global_; }; } // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.cpp b/src/providers/bttv/LoadBttvChannelEmote.cpp new file mode 100644 index 000000000..f5116cb02 --- /dev/null +++ b/src/providers/bttv/LoadBttvChannelEmote.cpp @@ -0,0 +1,14 @@ +#include "LoadBttvChannelEmote.hpp" + +#include +#include +#include +#include +#include "common/Common.hpp" +#include "common/NetworkRequest.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +} // namespace chatterino diff --git a/src/providers/bttv/LoadBttvChannelEmote.hpp b/src/providers/bttv/LoadBttvChannelEmote.hpp new file mode 100644 index 000000000..d0fa5cc32 --- /dev/null +++ b/src/providers/bttv/LoadBttvChannelEmote.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +class QString; + +namespace chatterino { + +} // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.cpp b/src/providers/chatterino/ChatterinoBadges.cpp new file mode 100644 index 000000000..98b1841d4 --- /dev/null +++ b/src/providers/chatterino/ChatterinoBadges.cpp @@ -0,0 +1,52 @@ +#include "ChatterinoBadges.hpp" + +#include +#include +#include +#include +#include "common/NetworkRequest.hpp" + +namespace chatterino { + +ChatterinoBadges::ChatterinoBadges() +{ +} + +boost::optional ChatterinoBadges::getBadge(const UserName &username) +{ + return this->badges.access()->get(username); +} + +void ChatterinoBadges::loadChatterinoBadges() +{ + static QString url("https://fourtf.com/chatterino/badges.json"); + + NetworkRequest req(url); + req.setCaller(QThread::currentThread()); + + req.onSuccess([this](auto result) { + auto jsonRoot = result.parseJson(); + auto badges = this->badges.access(); + auto replacement = badges->makeReplacment(); + + for (auto jsonBadge_ : jsonRoot.value("badges").toArray()) { + auto jsonBadge = jsonBadge_.toObject(); + + auto emote = Emote{ + EmoteName{}, ImageSet{Url{jsonBadge.value("image").toString()}}, + Tooltip{jsonBadge.value("tooltip").toString()}, Url{}}; + + for (auto jsonUser : jsonBadge.value("users").toArray()) { + replacement.add(UserName{jsonUser.toString()}, + std::move(emote)); + } + } + + replacement.apply(); + return Success; + }); + + req.execute(); +} + +} // namespace chatterino diff --git a/src/providers/chatterino/ChatterinoBadges.hpp b/src/providers/chatterino/ChatterinoBadges.hpp new file mode 100644 index 000000000..3162c75b1 --- /dev/null +++ b/src/providers/chatterino/ChatterinoBadges.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include "common/Common.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" +#include "messages/EmoteCache.hpp" + +namespace chatterino { + +class ChatterinoBadges +{ +public: + ChatterinoBadges(); + + boost::optional getBadge(const UserName &username); + +private: + void loadChatterinoBadges(); + + UniqueAccess> badges; +}; + +} // namespace chatterino diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index 10a0f28e6..06d4e798b 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -4,13 +4,19 @@ #include "debug/Log.hpp" #include "singletons/Settings.hpp" +#include +#include +#include #include +#include +#include namespace chatterino { namespace { -void parseEmoji(const std::shared_ptr &emojiData, const rapidjson::Value &unparsedEmoji, +void parseEmoji(const std::shared_ptr &emojiData, + const rapidjson::Value &unparsedEmoji, QString shortCode = QString()) { static uint unicodeBytes[4]; @@ -75,7 +81,8 @@ void parseEmoji(const std::shared_ptr &emojiData, const rapidjson::Va int numUnicodeBytes = 0; for (const QString &unicodeCharacter : unicodeCharacters) { - unicodeBytes[numUnicodeBytes++] = QString(unicodeCharacter).toUInt(nullptr, 16); + unicodeBytes[numUnicodeBytes++] = + QString(unicodeCharacter).toUInt(nullptr, 16); } emojiData->value = QString::fromUcs4(unicodeBytes, numUnicodeBytes); @@ -111,8 +118,8 @@ void Emojis::loadEmojis() rapidjson::ParseResult result = root.Parse(data.toUtf8(), data.length()); if (result.Code() != rapidjson::kParseErrorNone) { - Log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), - result.Offset()); + log("JSON parse error: {} ({})", + rapidjson::GetParseError_En(result.Code()), result.Offset()); return; } @@ -130,7 +137,8 @@ void Emojis::loadEmojis() this->emojis.insert(emojiData->unifiedCode, emojiData); if (unparsedEmoji.HasMember("skin_variations")) { - for (const auto &skinVariation : unparsedEmoji["skin_variations"].GetObject()) { + for (const auto &skinVariation : + unparsedEmoji["skin_variations"].GetObject()) { std::string tone = skinVariation.name.GetString(); const auto &variation = skinVariation.value; @@ -138,20 +146,23 @@ void Emojis::loadEmojis() auto toneNameIt = toneNames.find(tone); if (toneNameIt == toneNames.end()) { - Log("Tone with key {} does not exist in tone names map", tone); + log("Tone with key {} does not exist in tone names map", + tone); continue; } parseEmoji(variationEmojiData, variation, emojiData->shortCodes[0] + "_" + toneNameIt->second); - this->emojiShortCodeToEmoji_.insert(variationEmojiData->shortCodes[0], - variationEmojiData); + this->emojiShortCodeToEmoji_.insert( + variationEmojiData->shortCodes[0], variationEmojiData); this->shortCodes.push_back(variationEmojiData->shortCodes[0]); - this->emojiFirstByte_[variationEmojiData->value.at(0)].append(variationEmojiData); + this->emojiFirstByte_[variationEmojiData->value.at(0)].append( + variationEmojiData); - this->emojis.insert(variationEmojiData->unifiedCode, variationEmojiData); + this->emojis.insert(variationEmojiData->unifiedCode, + variationEmojiData); } } } @@ -191,14 +202,16 @@ void Emojis::loadEmojiOne2Capabilities() void Emojis::sortEmojis() { for (auto &p : this->emojiFirstByte_) { - std::stable_sort(p.begin(), p.end(), [](const auto &lhs, const auto &rhs) { - return lhs->value.length() > rhs->value.length(); - }); + std::stable_sort(p.begin(), p.end(), + [](const auto &lhs, const auto &rhs) { + return lhs->value.length() > rhs->value.length(); + }); } auto &p = this->shortCodes; - std::stable_sort(p.begin(), p.end(), - [](const auto &lhs, const auto &rhs) { return lhs < rhs; }); + std::stable_sort(p.begin(), p.end(), [](const auto &lhs, const auto &rhs) { + return lhs < rhs; + }); } void Emojis::loadEmojiSet() @@ -206,8 +219,9 @@ void Emojis::loadEmojiSet() auto app = getApp(); app->settings->emojiSet.connect([=](const auto &emojiSet, auto) { - Log("Using emoji set {}", emojiSet); - this->emojis.each([=](const auto &name, std::shared_ptr &emoji) { + log("Using emoji set {}", emojiSet); + this->emojis.each([=](const auto &name, + std::shared_ptr &emoji) { QString emojiSetToUse = emojiSet; // clang-format off static std::map emojiSets = { @@ -254,20 +268,24 @@ void Emojis::loadEmojiSet() } } code = code.toLower(); - QString urlPrefix = "https://cdnjs.cloudflare.com/ajax/libs/emojione/2.2.6/assets/png/"; + QString urlPrefix = "https://cdnjs.cloudflare.com/ajax/libs/" + "emojione/2.2.6/assets/png/"; auto it = emojiSets.find(emojiSetToUse); if (it != emojiSets.end()) { urlPrefix = it->second; } QString url = urlPrefix + code + ".png"; - emoji->emoteData.image1x = - new Image(url, 0.35, emoji->value, ":" + emoji->shortCodes[0] + ":
Emoji"); + emoji->emote = std::make_shared(Emote{ + EmoteName{emoji->value}, ImageSet{Image::fromUrl({url}, 0.35)}, + Tooltip{":" + emoji->shortCodes[0] + ":
Emoji"}, Url{}}); }); }); } -void Emojis::parse(std::vector> &parsedWords, const QString &text) +std::vector> Emojis::parse( + const QString &text) { + auto result = std::vector>(); int lastParsedEmojiEndIndex = 0; for (auto i = 0; i < text.length(); ++i) { @@ -323,16 +341,17 @@ void Emojis::parse(std::vector> &parsedWords, con int currentParsedEmojiFirstIndex = i; int currentParsedEmojiEndIndex = i + (matchedEmojiLength); - int charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex; + int charactersFromLastParsedEmoji = + currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex; if (charactersFromLastParsedEmoji > 0) { // Add characters inbetween emojis - parsedWords.emplace_back( - EmoteData(), text.mid(lastParsedEmojiEndIndex, charactersFromLastParsedEmoji)); + result.emplace_back(text.mid(lastParsedEmojiEndIndex, + charactersFromLastParsedEmoji)); } // Push the emoji as a word to parsedWords - parsedWords.push_back(std::tuple(matchedEmoji->emoteData, QString())); + result.emplace_back(matchedEmoji->emote); lastParsedEmojiEndIndex = currentParsedEmojiEndIndex; @@ -341,8 +360,10 @@ void Emojis::parse(std::vector> &parsedWords, con if (lastParsedEmojiEndIndex < text.length()) { // Add remaining characters - parsedWords.emplace_back(EmoteData(), text.mid(lastParsedEmojiEndIndex)); + result.emplace_back(text.mid(lastParsedEmojiEndIndex)); } + + return result; } QString Emojis::replaceShortCodes(const QString &text) @@ -357,7 +378,8 @@ QString Emojis::replaceShortCodes(const QString &text) auto capturedString = match.captured(); - QString matchString = capturedString.toLower().mid(1, capturedString.size() - 2); + QString matchString = + capturedString.toLower().mid(1, capturedString.size() - 2); auto emojiIt = this->emojiShortCodeToEmoji_.constFind(matchString); @@ -367,7 +389,8 @@ QString Emojis::replaceShortCodes(const QString &text) auto emojiData = emojiIt.value(); - ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData->value); + ret.replace(offset + match.capturedStart(), match.capturedLength(), + emojiData->value); offset += emojiData->value.size() - match.capturedLength(); } diff --git a/src/providers/emoji/Emojis.hpp b/src/providers/emoji/Emojis.hpp index dc56c4e5d..18bd33522 100644 --- a/src/providers/emoji/Emojis.hpp +++ b/src/providers/emoji/Emojis.hpp @@ -1,18 +1,20 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" +#include "messages/Emote.hpp" #include "util/ConcurrentMap.hpp" #include #include - +#include #include +#include +#include namespace chatterino { struct EmojiData { - // actual byte-representation of the emoji (i.e. \154075\156150 which is :male:) + // actual byte-representation of the emoji (i.e. \154075\156150 which is + // :male:) QString value; // i.e. 204e-50a2 @@ -26,7 +28,7 @@ struct EmojiData { std::vector variations; - EmoteData emoteData; + EmotePtr emote; }; using EmojiMap = ConcurrentMap>; @@ -36,7 +38,7 @@ class Emojis public: void initialize(); void load(); - void parse(std::vector> &parsedWords, const QString &text); + std::vector> parse(const QString &text); EmojiMap emojis; std::vector shortCodes; @@ -54,7 +56,8 @@ private: // shortCodeToEmoji maps strings like "sunglasses" to its emoji QMap> emojiShortCodeToEmoji_; - // Maps the first character of the emoji unicode string to a vector of possible emojis + // Maps the first character of the emoji unicode string to a vector of + // possible emojis QMap>> emojiFirstByte_; }; diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 9b7d470d0..24341a817 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -1,141 +1,160 @@ #include "providers/ffz/FfzEmotes.hpp" -#include "common/UrlFetch.hpp" +#include + +#include "common/NetworkRequest.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" namespace chatterino { - namespace { - -QString getEmoteLink(const QJsonObject &urls, const QString &emoteScale) +Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale) { auto emote = urls.value(emoteScale); if (emote.isUndefined()) { - return ""; + return {""}; } assert(emote.isString()); - return "https:" + emote.toString(); + return {"https:" + emote.toString()}; } - -void fillInEmoteData(const QJsonObject &urls, const QString &code, const QString &tooltip, - EmoteData &emoteData) +void fillInEmoteData(const QJsonObject &urls, const EmoteName &name, + const QString &tooltip, Emote &emoteData) { - QString url1x = getEmoteLink(urls, "1"); - QString url2x = getEmoteLink(urls, "2"); - QString url3x = getEmoteLink(urls, "4"); + auto url1x = getEmoteLink(urls, "1"); + auto url2x = getEmoteLink(urls, "2"); + auto url3x = getEmoteLink(urls, "4"); - assert(!url1x.isEmpty()); - - emoteData.image1x = new Image(url1x, 1, code, tooltip); - - if (!url2x.isEmpty()) { - emoteData.image2x = new Image(url2x, 0.5, code, tooltip); - } - - if (!url3x.isEmpty()) { - emoteData.image3x = new Image(url3x, 0.25, code, tooltip); - } + //, code, tooltip + emoteData.name = name; + emoteData.images = + ImageSet{Image::fromUrl(url1x, 1), Image::fromUrl(url2x, 0.5), + Image::fromUrl(url3x, 0.25)}; + emoteData.tooltip = {tooltip}; } +EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id) +{ + static std::unordered_map> cache; + static std::mutex mutex; + return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id); +} +std::pair parseGlobalEmotes(const QJsonObject &jsonRoot, + const EmoteMap ¤tEmotes) +{ + auto jsonSets = jsonRoot.value("sets").toObject(); + auto emotes = EmoteMap(); + + for (auto jsonSet : jsonSets) { + auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); + + for (auto jsonEmoteValue : jsonEmotes) { + auto jsonEmote = jsonEmoteValue.toObject(); + + auto name = EmoteName{jsonEmote.value("name").toString()}; + auto id = EmoteId{jsonEmote.value("id").toString()}; + auto urls = jsonEmote.value("urls").toObject(); + + auto emote = Emote(); + fillInEmoteData(urls, name, name.string + "
Global FFZ Emote", + emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + emotes[name] = + cachedOrMakeEmotePtr(std::move(emote), currentEmotes); + } + } + + return {Success, std::move(emotes)}; +} +std::pair parseChannelEmotes(const QJsonObject &jsonRoot) +{ + auto jsonSets = jsonRoot.value("sets").toObject(); + auto emotes = EmoteMap(); + + for (auto jsonSet : jsonSets) { + auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray(); + + for (auto _jsonEmote : jsonEmotes) { + auto jsonEmote = _jsonEmote.toObject(); + + // margins + auto id = EmoteId{QString::number(jsonEmote.value("id").toInt())}; + auto name = EmoteName{jsonEmote.value("name").toString()}; + auto urls = jsonEmote.value("urls").toObject(); + + Emote emote; + fillInEmoteData(urls, name, name.string + "
Channel FFZ Emote", + emote); + emote.homePage = + Url{QString("https://www.frankerfacez.com/emoticon/%1-%2") + .arg(id.string) + .arg(name.string)}; + + emotes[name] = cachedOrMake(std::move(emote), id); + } + } + + return {Success, std::move(emotes)}; +} } // namespace -void FFZEmotes::loadGlobalEmotes() +FfzEmotes::FfzEmotes() + : global_(std::make_shared()) +{ +} + +std::shared_ptr FfzEmotes::global() const +{ + return this->global_.get(); +} + +boost::optional FfzEmotes::global(const EmoteName &name) const +{ + auto emotes = this->global_.get(); + auto it = emotes->find(name); + if (it != emotes->end()) return it->second; + return boost::none; +} + +void FfzEmotes::loadGlobal() { QString url("https://api.frankerfacez.com/v1/set/global"); NetworkRequest request(url); request.setCaller(QThread::currentThread()); request.setTimeout(30000); - request.setUseQuickLoadCache(true); - request.onSuccess([this](auto result) { - auto root = result.parseJson(); - auto sets = root.value("sets").toObject(); - std::vector codes; - for (const QJsonValue &set : sets) { - auto emoticons = set.toObject().value("emoticons").toArray(); - - for (const QJsonValue &emote : emoticons) { - QJsonObject object = emote.toObject(); - - QString code = object.value("name").toString(); - int id = object.value("id").toInt(); - QJsonObject urls = object.value("urls").toObject(); - - EmoteData emoteData; - fillInEmoteData(urls, code, code + "
Global FFZ Emote", emoteData); - emoteData.pageLink = - QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); - - this->globalEmotes.insert(code, emoteData); - codes.push_back(code); - } - - this->globalEmoteCodes = codes; - } - - return true; + request.onSuccess([this](auto result) -> Outcome { + auto emotes = this->global(); + auto pair = parseGlobalEmotes(result.parseJson(), *emotes); + if (pair.first) + this->global_.set( + std::make_shared(std::move(pair.second))); + return pair.first; }); request.execute(); } -void FFZEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) +void FfzEmotes::loadChannel(const QString &channelName, + std::function callback) { - printf("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", qPrintable(channelName)); + log("[FFZEmotes] Reload FFZ Channel Emotes for channel %s\n", channelName); - QString url("https://api.frankerfacez.com/v1/room/" + channelName); - - NetworkRequest request(url); + NetworkRequest request("https://api.frankerfacez.com/v1/room/" + + channelName); request.setCaller(QThread::currentThread()); request.setTimeout(3000); - request.setUseQuickLoadCache(true); - request.onSuccess([this, channelName, _map](auto result) { - auto rootNode = result.parseJson(); - auto map = _map.lock(); - if (_map.expired()) { - return false; - } - - map->clear(); - - auto setsNode = rootNode.value("sets").toObject(); - - std::vector codes; - for (const QJsonValue &setNode : setsNode) { - auto emotesNode = setNode.toObject().value("emoticons").toArray(); - - for (const QJsonValue &emoteNode : emotesNode) { - QJsonObject emoteObject = emoteNode.toObject(); - - // margins - int id = emoteObject.value("id").toInt(); - QString code = emoteObject.value("name").toString(); - - QJsonObject urls = emoteObject.value("urls").toObject(); - - auto emote = this->channelEmoteCache_.getOrAdd(id, [id, &code, &urls] { - EmoteData emoteData; - fillInEmoteData(urls, code, code + "
Channel FFZ Emote", emoteData); - emoteData.pageLink = - QString("https://www.frankerfacez.com/emoticon/%1-%2").arg(id).arg(code); - - return emoteData; - }); - - this->channelEmotes.insert(code, emote); - map->insert(code, emote); - codes.push_back(code); - } - - this->channelEmoteCodes[channelName] = codes; - } - - return true; + request.onSuccess([callback = std::move(callback)](auto result) -> Outcome { + auto pair = parseChannelEmotes(result.parseJson()); + if (pair.first) callback(std::move(pair.second)); + return pair.first; }); request.execute(); diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index b216de0b0..cc75b09bd 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -1,27 +1,29 @@ #pragma once -#include "common/Emotemap.hpp" -#include "common/SimpleSignalVector.hpp" -#include "util/ConcurrentMap.hpp" - -#include +#include +#include "common/Atomic.hpp" +#include "messages/Emote.hpp" namespace chatterino { -class FFZEmotes +class FfzEmotes final { + static constexpr const char *globalEmoteApiUrl = + "https://api.frankerfacez.com/v1/set/global"; + static constexpr const char *channelEmoteApiUrl = + "https://api.betterttv.net/2/channels/"; + public: - EmoteMap globalEmotes; - SimpleSignalVector globalEmoteCodes; + FfzEmotes(); - EmoteMap channelEmotes; - std::map> channelEmoteCodes; - - void loadGlobalEmotes(); - void loadChannelEmotes(const QString &channelName, std::weak_ptr channelEmoteMap); + std::shared_ptr global() const; + boost::optional global(const EmoteName &name) const; + void loadGlobal(); + static void loadChannel(const QString &channelName, + std::function callback); private: - ConcurrentMap channelEmoteCache_; + Atomic> global_; }; } // namespace chatterino diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 592ce4ff1..b7995282f 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -3,6 +3,7 @@ #include "common/Common.hpp" #include "messages/LimitedQueueSnapshot.hpp" #include "messages/Message.hpp" +#include "messages/MessageBuilder.hpp" #include @@ -12,49 +13,49 @@ AbstractIrcServer::AbstractIrcServer() { // Initialize the connections this->writeConnection_.reset(new IrcConnection); - this->writeConnection_->moveToThread(QCoreApplication::instance()->thread()); + this->writeConnection_->moveToThread( + QCoreApplication::instance()->thread()); - QObject::connect(this->writeConnection_.get(), &Communi::IrcConnection::messageReceived, - [this](auto msg) { this->writeConnectionMessageReceived(msg); }); + QObject::connect( + this->writeConnection_.get(), &Communi::IrcConnection::messageReceived, + [this](auto msg) { this->writeConnectionMessageReceived(msg); }); // Listen to read connection message signals this->readConnection_.reset(new IrcConnection); this->readConnection_->moveToThread(QCoreApplication::instance()->thread()); - QObject::connect(this->readConnection_.get(), &Communi::IrcConnection::messageReceived, + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::messageReceived, [this](auto msg) { this->messageReceived(msg); }); - QObject::connect(this->readConnection_.get(), &Communi::IrcConnection::privateMessageReceived, + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::privateMessageReceived, [this](auto msg) { this->privateMessageReceived(msg); }); - QObject::connect(this->readConnection_.get(), &Communi::IrcConnection::connected, + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::connected, [this] { this->onConnected(); }); - QObject::connect(this->readConnection_.get(), &Communi::IrcConnection::disconnected, + QObject::connect(this->readConnection_.get(), + &Communi::IrcConnection::disconnected, [this] { this->onDisconnected(); }); // listen to reconnect request - this->readConnection_->reconnectRequested.connect([this] { this->connect(); }); - // this->writeConnection->reconnectRequested.connect([this] { this->connect(); }); -} - -IrcConnection *AbstractIrcServer::getReadConnection() const -{ - return this->readConnection_.get(); -} - -IrcConnection *AbstractIrcServer::getWriteConnection() const -{ - return this->writeConnection_.get(); + this->readConnection_->reconnectRequested.connect( + [this] { this->connect(); }); + // this->writeConnection->reconnectRequested.connect([this] { + // this->connect(); }); } void AbstractIrcServer::connect() { this->disconnect(); - // if (this->hasSeparateWriteConnection()) { - this->initializeConnection(this->writeConnection_.get(), false, true); - this->initializeConnection(this->readConnection_.get(), true, false); - // } else { - // this->initializeConnection(this->readConnection.get(), true, true); - // } + bool separateWriteConnection = this->hasSeparateWriteConnection(); + + if (separateWriteConnection) { + this->initializeConnection(this->writeConnection_.get(), false, true); + this->initializeConnection(this->readConnection_.get(), true, false); + } else { + this->initializeConnection(this->readConnection_.get(), true, true); + } // fourtf: this should be asynchronous { @@ -62,13 +63,9 @@ void AbstractIrcServer::connect() std::lock_guard lock2(this->channelMutex); for (std::weak_ptr &weak : this->channels.values()) { - std::shared_ptr chan = weak.lock(); - if (!chan) { - continue; + if (auto channel = std::shared_ptr(weak.lock())) { + this->readConnection_->sendRaw("JOIN #" + channel->getName()); } - - this->writeConnection_->sendRaw("JOIN #" + chan->name); - this->readConnection_->sendRaw("JOIN #" + chan->name); } this->writeConnection_->open(); @@ -87,22 +84,30 @@ void AbstractIrcServer::disconnect() this->writeConnection_->close(); } -void AbstractIrcServer::sendMessage(const QString &channelName, const QString &message) +void AbstractIrcServer::sendMessage(const QString &channelName, + const QString &message) +{ + this->sendRawMessage("PRIVMSG #" + channelName + " :" + message); +} + +void AbstractIrcServer::sendRawMessage(const QString &rawMessage) { std::lock_guard locker(this->connectionMutex_); - // fourtf: trim the message if it's sent from twitch chat - - if (this->writeConnection_) { - this->writeConnection_->sendRaw("PRIVMSG #" + channelName + " :" + message); + if (this->hasSeparateWriteConnection()) { + this->writeConnection_->sendRaw(rawMessage); + } else { + this->readConnection_->sendRaw(rawMessage); } } -void AbstractIrcServer::writeConnectionMessageReceived(Communi::IrcMessage *message) +void AbstractIrcServer::writeConnectionMessageReceived( + Communi::IrcMessage *message) { } -std::shared_ptr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) +std::shared_ptr AbstractIrcServer::getOrAddChannel( + const QString &dirtyChannelName) { auto channelName = this->cleanChannelName(dirtyChannelName); @@ -126,7 +131,8 @@ std::shared_ptr AbstractIrcServer::getOrAddChannel(const QString &dirty chan->destroyed.connect([this, clojuresInCppAreShit] { // fourtf: issues when the server itself is destroyed - Log("[AbstractIrcServer::addChannel] {} was destroyed", clojuresInCppAreShit); + log("[AbstractIrcServer::addChannel] {} was destroyed", + clojuresInCppAreShit); this->channels.remove(clojuresInCppAreShit); if (this->readConnection_) { @@ -154,7 +160,8 @@ std::shared_ptr AbstractIrcServer::getOrAddChannel(const QString &dirty return chan; } -std::shared_ptr AbstractIrcServer::getChannelOrEmpty(const QString &dirtyChannelName) +std::shared_ptr AbstractIrcServer::getChannelOrEmpty( + const QString &dirtyChannelName) { auto channelName = this->cleanChannelName(dirtyChannelName); @@ -183,8 +190,8 @@ void AbstractIrcServer::onConnected() { std::lock_guard lock(this->channelMutex); - MessagePtr connMsg = Message::createSystemMessage("connected to chat"); - MessagePtr reconnMsg = Message::createSystemMessage("reconnected to chat"); + auto connected = makeSystemMessage("connected to chat"); + auto reconnected = makeSystemMessage("reconnected to chat"); for (std::weak_ptr &weak : this->channels.values()) { std::shared_ptr chan = weak.lock(); @@ -194,16 +201,17 @@ void AbstractIrcServer::onConnected() LimitedQueueSnapshot snapshot = chan->getMessageSnapshot(); - bool replaceMessage = - snapshot.getLength() > 0 && - snapshot[snapshot.getLength() - 1]->flags & Message::DisconnectedMessage; + bool replaceMessage = snapshot.getLength() > 0 && + snapshot[snapshot.getLength() - 1]->flags.has( + MessageFlag::DisconnectedMessage); if (replaceMessage) { - chan->replaceMessage(snapshot[snapshot.getLength() - 1], reconnMsg); + chan->replaceMessage(snapshot[snapshot.getLength() - 1], + reconnected); continue; } - chan->addMessage(connMsg); + chan->addMessage(connected); } } @@ -211,8 +219,9 @@ void AbstractIrcServer::onDisconnected() { std::lock_guard lock(this->channelMutex); - MessagePtr msg = Message::createSystemMessage("disconnected from chat"); - msg->flags |= Message::DisconnectedMessage; + MessageBuilder b(systemMessage, "disconnected from chat"); + b->flags.set(MessageFlag::DisconnectedMessage); + auto disconnected = b.release(); for (std::weak_ptr &weak : this->channels.values()) { std::shared_ptr chan = weak.lock(); @@ -220,11 +229,12 @@ void AbstractIrcServer::onDisconnected() continue; } - chan->addMessage(msg); + chan->addMessage(disconnected); } } -std::shared_ptr AbstractIrcServer::getCustomChannel(const QString &channelName) +std::shared_ptr AbstractIrcServer::getCustomChannel( + const QString &channelName) { return nullptr; } @@ -236,16 +246,19 @@ QString AbstractIrcServer::cleanChannelName(const QString &dirtyChannelName) void AbstractIrcServer::addFakeMessage(const QString &data) { - auto fakeMessage = Communi::IrcMessage::fromData(data.toUtf8(), this->readConnection_.get()); + auto fakeMessage = Communi::IrcMessage::fromData( + data.toUtf8(), this->readConnection_.get()); if (fakeMessage->command() == "PRIVMSG") { - this->privateMessageReceived(static_cast(fakeMessage)); + this->privateMessageReceived( + static_cast(fakeMessage)); } else { this->messageReceived(fakeMessage); } } -void AbstractIrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message) +void AbstractIrcServer::privateMessageReceived( + Communi::IrcPrivateMessage *message) { } diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 9e2723c39..580cdd0fe 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -17,13 +17,11 @@ public: virtual ~AbstractIrcServer() = default; // connection - IrcConnection *getReadConnection() const; - IrcConnection *getWriteConnection() const; - void connect(); void disconnect(); void sendMessage(const QString &channelName, const QString &message); + void sendRawMessage(const QString &rawMessage); // channels std::shared_ptr getOrAddChannel(const QString &dirtyChannelName); @@ -32,7 +30,8 @@ public: // signals pajlada::Signals::NoArgSignal connected; pajlada::Signals::NoArgSignal disconnected; - // pajlada::Signals::Signal onPrivateMessage; + // pajlada::Signals::Signal + // onPrivateMessage; void addFakeMessage(const QString &data); @@ -42,8 +41,10 @@ public: protected: AbstractIrcServer(); - virtual void initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) = 0; - virtual std::shared_ptr createChannel(const QString &channelName) = 0; + virtual void initializeConnection(IrcConnection *connection, bool isRead, + bool isWrite) = 0; + virtual std::shared_ptr createChannel( + const QString &channelName) = 0; virtual void privateMessageReceived(Communi::IrcPrivateMessage *message); virtual void messageReceived(Communi::IrcMessage *message); @@ -52,8 +53,10 @@ protected: virtual void onConnected(); virtual void onDisconnected(); - virtual std::shared_ptr getCustomChannel(const QString &channelName); + virtual std::shared_ptr getCustomChannel( + const QString &channelName); + virtual bool hasSeparateWriteConnection() const = 0; virtual QString cleanChannelName(const QString &dirtyChannelName); QMap> channels; diff --git a/src/providers/irc/IrcAccount.cpp b/src/providers/irc/IrcAccount.cpp index 1b7afad50..96486b7ea 100644 --- a/src/providers/irc/IrcAccount.cpp +++ b/src/providers/irc/IrcAccount.cpp @@ -2,7 +2,8 @@ // namespace chatterino { // -// IrcAccount::IrcAccount(const QString &_userName, const QString &_nickName, const QString +// IrcAccount::IrcAccount(const QString &_userName, const QString &_nickName, +// const QString // &_realName, // const QString &_password) // : userName(_userName) diff --git a/src/providers/irc/IrcAccount.hpp b/src/providers/irc/IrcAccount.hpp index 5906026c6..2c4345b10 100644 --- a/src/providers/irc/IrcAccount.hpp +++ b/src/providers/irc/IrcAccount.hpp @@ -7,7 +7,8 @@ // class IrcAccount //{ // public: -// IrcAccount(const QString &userName, const QString &nickName, const QString &realName, +// IrcAccount(const QString &userName, const QString &nickName, const QString +// &realName, // const QString &password); // const QString &getUserName() const; diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 82d0d1797..4739739aa 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -9,26 +9,32 @@ IrcConnection::IrcConnection(QObject *parent) this->pingTimer_.setInterval(5000); this->pingTimer_.start(); QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] { - if (!this->recentlyReceivedMessage_.load()) { - this->sendRaw("PING"); - this->reconnectTimer_.start(); + if (this->isConnected()) { + if (!this->recentlyReceivedMessage_.load()) { + this->sendRaw("PING"); + this->reconnectTimer_.start(); + } + this->recentlyReceivedMessage_ = false; } - this->recentlyReceivedMessage_ = false; }); // reconnect after x seconds without receiving a message this->reconnectTimer_.setInterval(5000); this->reconnectTimer_.setSingleShot(true); - QObject::connect(&this->reconnectTimer_, &QTimer::timeout, - [this] { reconnectRequested.invoke(); }); - - QObject::connect(this, &Communi::IrcConnection::messageReceived, [this](Communi::IrcMessage *) { - this->recentlyReceivedMessage_ = true; - - if (this->reconnectTimer_.isActive()) { - this->reconnectTimer_.stop(); + QObject::connect(&this->reconnectTimer_, &QTimer::timeout, [this] { + if (this->isConnected()) { + reconnectRequested.invoke(); } }); + + QObject::connect(this, &Communi::IrcConnection::messageReceived, + [this](Communi::IrcMessage *) { + this->recentlyReceivedMessage_ = true; + + if (this->reconnectTimer_.isActive()) { + this->reconnectTimer_.stop(); + } + }); } } // namespace chatterino diff --git a/src/providers/irc/IrcServer.hpp b/src/providers/irc/IrcServer.hpp index 874179b47..c8faf434c 100644 --- a/src/providers/irc/IrcServer.hpp +++ b/src/providers/irc/IrcServer.hpp @@ -14,7 +14,8 @@ namespace chatterino { // std::shared_ptr getAccount() const; // protected: -// virtual void initializeConnection(Communi::IrcConnection *connection, bool isReadConnection); +// virtual void initializeConnection(Communi::IrcConnection *connection, bool +// isReadConnection); // virtual void privateMessageReceived(Communi::IrcPrivateMessage *message); // virtual void messageReceived(Communi::IrcMessage *message); diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 904c772c8..35c7c51d9 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -25,15 +25,17 @@ IrcMessageHandler &IrcMessageHandler::getInstance() return instance; } -void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server) +void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, + TwitchServer &server) { - this->addMessage(message, message->target(), message->content(), server, false, - message->isAction()); + this->addMessage(message, message->target(), message->content(), server, + false, message->isAction()); } -void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, const QString &target, - const QString &content, TwitchServer &server, bool isSub, - bool isAction) +void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, + const QString &target, + const QString &content, TwitchServer &server, + bool isSub, bool isAction) { QString channelName; if (!trimChannelName(target, channelName)) { @@ -58,13 +60,16 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, const QString TwitchMessageBuilder builder(chan.get(), _message, args, content, isAction); if (isSub || !builder.isIgnored()) { - MessagePtr msg = builder.build(); - if (isSub) { - msg->flags |= Message::Subscription; - msg->flags &= ~Message::Highlighted; - } else { - if (msg->flags & Message::Highlighted) { + builder->flags.set(MessageFlag::Subscription); + builder->flags.unset(MessageFlag::Highlighted); + } + + auto highlighted = builder->flags.has(MessageFlag::Highlighted); + auto msg = builder.build(); + + if (!isSub) { + if (highlighted) { server.mentionsChannel->addMessage(msg); getApp()->highlights->addHighlight(msg); } @@ -85,40 +90,40 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message) return; } auto chan = app->twitch.server->getChannelOrEmpty(chanName); - TwitchChannel *twitchChannel = dynamic_cast(chan.get()); - if (twitchChannel) { + if (auto *twitchChannel = dynamic_cast(chan.get())) { // room-id decltype(tags.find("xD")) it; if ((it = tags.find("room-id")) != tags.end()) { - auto roomID = it.value().toString(); + auto roomId = it.value().toString(); - twitchChannel->setRoomID(roomID); - - app->resources->loadChannelData(roomID); + twitchChannel->setRoomId(roomId); } // Room modes - TwitchChannel::RoomModes roomModes = twitchChannel->getRoomModes(); + { + auto roomModes = *twitchChannel->accessRoomModes(); - if ((it = tags.find("emote-only")) != tags.end()) { - roomModes.emoteOnly = it.value() == "1"; - } - if ((it = tags.find("subs-only")) != tags.end()) { - roomModes.submode = it.value() == "1"; - } - if ((it = tags.find("slow")) != tags.end()) { - roomModes.slowMode = it.value().toInt(); - } - if ((it = tags.find("r9k")) != tags.end()) { - roomModes.r9k = it.value() == "1"; - } - if ((it = tags.find("broadcaster-lang")) != tags.end()) { - roomModes.broadcasterLang = it.value().toString(); + if ((it = tags.find("emote-only")) != tags.end()) { + roomModes.emoteOnly = it.value() == "1"; + } + if ((it = tags.find("subs-only")) != tags.end()) { + roomModes.submode = it.value() == "1"; + } + if ((it = tags.find("slow")) != tags.end()) { + roomModes.slowMode = it.value().toInt(); + } + if ((it = tags.find("r9k")) != tags.end()) { + roomModes.r9k = it.value() == "1"; + } + if ((it = tags.find("broadcaster-lang")) != tags.end()) { + roomModes.broadcasterLang = it.value().toString(); + } + twitchChannel->setRoomModes(roomModes); } - twitchChannel->setRoomModes(roomModes); + twitchChannel->roomModesChanged.invoke(); } } @@ -140,14 +145,17 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) auto chan = app->twitch.server->getChannelOrEmpty(chanName); if (chan->isEmpty()) { - Log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not found", chanName); + log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not " + "found", + chanName); return; } // check if the chat has been cleared by a moderator if (message->parameters().length() == 1) { chan->disableAllMessages(); - chan->addMessage(Message::createSystemMessage("Chat has been cleared by a moderator.")); + chan->addMessage( + makeSystemMessage("Chat has been cleared by a moderator.")); return; } @@ -165,7 +173,9 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) reason = v.toString(); } - auto timeoutMsg = Message::createTimeoutMessage(username, durationInSeconds, reason, false); + auto timeoutMsg = MessageBuilder(timeoutMessage, username, + durationInSeconds, reason, false) + .release(); chan->addOrReplaceTimeout(timeoutMsg); // refresh all @@ -199,28 +209,27 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) { auto app = getApp(); - Log("Received whisper!"); + log("Received whisper!"); MessageParseArgs args; args.isReceivedWhisper = true; auto c = app->twitch.server->whispersChannel.get(); - TwitchMessageBuilder builder(c, message, args, message->parameter(1), false); + TwitchMessageBuilder builder(c, message, args, message->parameter(1), + false); if (!builder.isIgnored()) { + app->twitch.server->lastUserThatWhisperedMe.set(builder.userName); + MessagePtr _message = builder.build(); - if (_message->flags & Message::Highlighted) { + if (_message->flags.has(MessageFlag::Highlighted)) { app->twitch.server->mentionsChannel->addMessage(_message); } - app->twitch.server->lastUserThatWhisperedMe.set(builder.userName); - c->addMessage(_message); - _message->flags |= Message::DoNotTriggerNotification; - if (app->settings->inlineWhispers) { app->twitch.server->forEachChannel([_message](ChannelPtr channel) { channel->addMessage(_message); // @@ -229,7 +238,8 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) } } -void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server) +void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, + TwitchServer &server) { auto data = message->toData(); @@ -244,7 +254,8 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, Tw } if (msgType == "sub" || msgType == "resub" || msgType == "subgift") { - // Sub-specific message. I think it's only allowed for "resub" messages atm + // Sub-specific message. I think it's only allowed for "resub" messages + // atm if (!content.isEmpty()) { this->addMessage(message, target, content, server, true, false); } @@ -253,9 +264,11 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, Tw auto it = tags.find("system-msg"); if (it != tags.end()) { - auto newMessage = Message::createSystemMessage(parseTagString(it.value().toString())); + auto b = MessageBuilder(systemMessage, + parseTagString(it.value().toString())); - newMessage->flags |= Message::Subscription; + b->flags.set(MessageFlag::Subscription); + auto newMessage = b.release(); QString channelName; @@ -279,7 +292,8 @@ void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message) { auto app = getApp(); - auto channel = app->twitch.server->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = app->twitch.server->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); if (channel->isEmpty()) { return; @@ -295,14 +309,16 @@ void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message) void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { auto app = getApp(); - MessagePtr msg = Message::createSystemMessage(message->content()); + MessagePtr msg = makeSystemMessage(message->content()); QString channelName; if (!trimChannelName(message->target(), channelName)) { - // Notice wasn't targeted at a single channel, send to all twitch channels - app->twitch.server->forEachChannelAndSpecialChannels([msg](const auto &c) { - c->addMessage(msg); // - }); + // Notice wasn't targeted at a single channel, send to all twitch + // channels + app->twitch.server->forEachChannelAndSpecialChannels( + [msg](const auto &c) { + c->addMessage(msg); // + }); return; } @@ -310,7 +326,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) auto channel = app->twitch.server->getChannelOrEmpty(channelName); if (channel->isEmpty()) { - Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel manager ", + log("[IrcManager:handleNoticeMessage] Channel {} not found in channel " + "manager ", channelName); return; } @@ -318,7 +335,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) channel->addMessage(msg); } -void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message) +void IrcMessageHandler::handleWriteConnectionNoticeMessage( + Communi::IrcNoticeMessage *message) { static std::unordered_set readConnectionOnlyIDs{ "host_on", @@ -333,8 +351,9 @@ void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMes "r9k_on", "r9k_off", - // Display for user who times someone out. This implies you're a moderator, at which point - // you will be connected to PubSub and receive a better message from there + // Display for user who times someone out. This implies you're a + // moderator, at which point you will be connected to PubSub and receive + // a better message from there "timeout_success", "ban_success", }; @@ -347,7 +366,8 @@ void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMes return; } - Log("Showing notice message from write connection with message id '{}'", msgID); + log("Showing notice message from write connection with message id '{}'", + msgID); } this->handleNoticeMessage(message); @@ -356,9 +376,11 @@ void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMes void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) { auto app = getApp(); - auto channel = app->twitch.server->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = app->twitch.server->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); - if (TwitchChannel *twitchChannel = dynamic_cast(channel.get())) { + if (TwitchChannel *twitchChannel = + dynamic_cast(channel.get())) { twitchChannel->addJoinedUser(message->nick()); } } @@ -366,9 +388,11 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message) void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) { auto app = getApp(); - auto channel = app->twitch.server->getChannelOrEmpty(message->parameter(0).remove(0, 1)); + auto channel = app->twitch.server->getChannelOrEmpty( + message->parameter(0).remove(0, 1)); - if (TwitchChannel *twitchChannel = dynamic_cast(channel.get())) { + if (TwitchChannel *twitchChannel = + dynamic_cast(channel.get())) { twitchChannel->addPartedUser(message->nick()); } } diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index 084c88bcd..40f1efb12 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -13,13 +13,15 @@ class IrcMessageHandler public: static IrcMessageHandler &getInstance(); - void handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server); + void handlePrivMessage(Communi::IrcPrivateMessage *message, + TwitchServer &server); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message); void handleWhisperMessage(Communi::IrcMessage *message); - void handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server); + void handleUserNoticeMessage(Communi::IrcMessage *message, + TwitchServer &server); void handleModeMessage(Communi::IrcMessage *message); void handleNoticeMessage(Communi::IrcNoticeMessage *message); void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message); @@ -28,8 +30,9 @@ public: void handlePartMessage(Communi::IrcMessage *message); private: - void addMessage(Communi::IrcMessage *message, const QString &target, const QString &content, - TwitchServer &server, bool isResub, bool isAction); + void addMessage(Communi::IrcMessage *message, const QString &target, + const QString &content, TwitchServer &server, bool isResub, + bool isAction); }; } // namespace chatterino diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp index 2033e977d..c3d9e5102 100644 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -4,6 +4,7 @@ #include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include #include namespace chatterino { @@ -24,7 +25,8 @@ PartialTwitchUser PartialTwitchUser::byId(const QString &id) return user; } -void PartialTwitchUser::getId(std::function successCallback, const QObject *caller) +void PartialTwitchUser::getId(std::function successCallback, + const QObject *caller) { assert(!this->username_.isEmpty()); @@ -32,36 +34,38 @@ void PartialTwitchUser::getId(std::function successCallback, cons caller = QThread::currentThread(); } - NetworkRequest request("https://api.twitch.tv/kraken/users?login=" + this->username_); + NetworkRequest request("https://api.twitch.tv/kraken/users?login=" + + this->username_); request.setCaller(caller); request.makeAuthorizedV5(getDefaultClientID()); - request.onSuccess([successCallback](auto result) { + request.onSuccess([successCallback](auto result) -> Outcome { auto root = result.parseJson(); if (!root.value("users").isArray()) { - Log("API Error while getting user id, users is not an array"); - return false; + log("API Error while getting user id, users is not an array"); + 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"); - return false; + log("API Error while getting user id, users array size is not 1"); + return Failure; } if (!users[0].isObject()) { - Log("API Error while getting user id, first user is not an object"); - return false; + log("API Error while getting user id, first user is not an object"); + return Failure; } auto firstUser = users[0].toObject(); auto id = firstUser.value("_id"); if (!id.isString()) { - Log("API Error: while getting user id, first user object `_id` key is not a " + log("API Error: while getting user id, first user object `_id` key " + "is not a " "string"); - return false; + return Failure; } successCallback(id.toString()); - return true; + return Success; }); request.execute(); diff --git a/src/providers/twitch/PartialTwitchUser.hpp b/src/providers/twitch/PartialTwitchUser.hpp index a868f6fea..36876dc26 100644 --- a/src/providers/twitch/PartialTwitchUser.hpp +++ b/src/providers/twitch/PartialTwitchUser.hpp @@ -19,7 +19,8 @@ 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, + const QObject *caller = nullptr); }; } // namespace chatterino diff --git a/src/providers/twitch/PubsubActions.hpp b/src/providers/twitch/PubsubActions.hpp index 095cbad85..688468f0e 100644 --- a/src/providers/twitch/PubsubActions.hpp +++ b/src/providers/twitch/PubsubActions.hpp @@ -21,7 +21,8 @@ struct PubSubAction { QString roomID; }; -// Used when a chat mode (i.e. slowmode, subscribers only mode) is enabled or disabled +// Used when a chat mode (i.e. slowmode, subscribers only mode) is enabled or +// disabled struct ModeChangedAction : PubSubAction { using PubSubAction::PubSubAction; diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 726f54915..7f591aebc 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -24,7 +24,8 @@ static std::map sentMessages; namespace detail { -PubSubClient::PubSubClient(WebsocketClient &websocketClient, WebsocketHandle handle) +PubSubClient::PubSubClient(WebsocketClient &websocketClient, + WebsocketHandle handle) : websocketClient_(websocketClient) , handle_(handle) { @@ -58,7 +59,8 @@ bool PubSubClient::listen(rapidjson::Document &message) this->numListens_ += numRequestedListens; for (const auto &topic : message["data"]["topics"].GetArray()) { - this->listeners_.emplace_back(Listener{topic.GetString(), false, false, false}); + this->listeners_.emplace_back( + Listener{topic.GetString(), false, false, false}); } auto uuid = CreateUUID(); @@ -107,7 +109,7 @@ void PubSubClient::handlePong() { assert(this->awaitingPong_); - Log("Got pong!"); + log("Got pong!"); this->awaitingPong_ = false; } @@ -135,34 +137,38 @@ void PubSubClient::ping() auto self = this->shared_from_this(); - runAfter(this->websocketClient_.get_io_service(), std::chrono::seconds(15), [self](auto timer) { - if (!self->started_) { - return; - } + runAfter(this->websocketClient_.get_io_service(), std::chrono::seconds(15), + [self](auto timer) { + if (!self->started_) { + return; + } - if (self->awaitingPong_) { - Log("No pong respnose, disconnect!"); - // TODO(pajlada): Label this connection as "disconnect me" - } - }); + if (self->awaitingPong_) { + log("No pong respnose, disconnect!"); + // TODO(pajlada): Label this connection as "disconnect me" + } + }); - runAfter(this->websocketClient_.get_io_service(), std::chrono::minutes(5), [self](auto timer) { - if (!self->started_) { - return; - } + runAfter(this->websocketClient_.get_io_service(), std::chrono::minutes(5), + [self](auto timer) { + if (!self->started_) { + return; + } - self->ping(); // - }); + self->ping(); // + }); } bool PubSubClient::send(const char *payload) { WebsocketErrorCode ec; - this->websocketClient_.send(this->handle_, payload, websocketpp::frame::opcode::text, ec); + this->websocketClient_.send(this->handle_, payload, + websocketpp::frame::opcode::text, ec); if (ec) { - Log("Error sending message {}: {}", payload, ec.message()); - // TODO(pajlada): Check which error code happened and maybe gracefully handle it + log("Error sending message {}: {}", payload, ec.message()); + // TODO(pajlada): Check which error code happened and maybe gracefully + // handle it return false; } @@ -176,13 +182,15 @@ PubSub::PubSub() { qDebug() << "init PubSub"; - this->moderationActionHandlers["clear"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["clear"] = [this](const auto &data, + const auto &roomID) { ClearChatAction action(data, roomID); this->signals_.moderation.chatCleared.invoke(action); }; - this->moderationActionHandlers["slowoff"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["slowoff"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::Slow; @@ -191,33 +199,34 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["slow"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["slow"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::Slow; action.state = ModeChangedAction::State::On; if (!data.HasMember("args")) { - Log("Missing required args member"); + log("Missing required args member"); return; } const auto &args = data["args"]; if (!args.IsArray()) { - Log("args member must be an array"); + log("args member must be an array"); return; } if (args.Size() == 0) { - Log("Missing duration argument in slowmode on"); + log("Missing duration argument in slowmode on"); return; } const auto &durationArg = args[0]; if (!durationArg.IsString()) { - Log("Duration arg must be a string"); + log("Duration arg must be a string"); return; } @@ -228,7 +237,8 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::R9K; @@ -237,7 +247,8 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["r9kbeta"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["r9kbeta"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::R9K; @@ -246,17 +257,18 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["subscribersoff"] = [this](const auto &data, - const auto &roomID) { - ModeChangedAction action(data, roomID); + this->moderationActionHandlers["subscribersoff"] = + [this](const auto &data, const auto &roomID) { + ModeChangedAction action(data, roomID); - action.mode = ModeChangedAction::Mode::SubscribersOnly; - action.state = ModeChangedAction::State::Off; + action.mode = ModeChangedAction::Mode::SubscribersOnly; + action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); - }; + this->signals_.moderation.modeChanged.invoke(action); + }; - this->moderationActionHandlers["subscribers"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["subscribers"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::SubscribersOnly; @@ -265,16 +277,18 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["emoteonlyoff"] = [this](const auto &data, const auto &roomID) { - ModeChangedAction action(data, roomID); + this->moderationActionHandlers["emoteonlyoff"] = + [this](const auto &data, const auto &roomID) { + ModeChangedAction action(data, roomID); - action.mode = ModeChangedAction::Mode::EmoteOnly; - action.state = ModeChangedAction::State::Off; + action.mode = ModeChangedAction::Mode::EmoteOnly; + action.state = ModeChangedAction::State::Off; - this->signals_.moderation.modeChanged.invoke(action); - }; + this->signals_.moderation.modeChanged.invoke(action); + }; - this->moderationActionHandlers["emoteonly"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["emoteonly"] = [this](const auto &data, + const auto &roomID) { ModeChangedAction action(data, roomID); action.mode = ModeChangedAction::Mode::EmoteOnly; @@ -283,7 +297,8 @@ PubSub::PubSub() this->signals_.moderation.modeChanged.invoke(action); }; - this->moderationActionHandlers["unmod"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["unmod"] = [this](const auto &data, + const auto &roomID) { ModerationStateAction action(data, roomID); getTargetUser(data, action.target); @@ -299,7 +314,7 @@ PubSub::PubSub() return; } } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } action.modded = false; @@ -307,7 +322,8 @@ PubSub::PubSub() this->signals_.moderation.moderationStateChanged.invoke(action); }; - this->moderationActionHandlers["mod"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["mod"] = [this](const auto &data, + const auto &roomID) { ModerationStateAction action(data, roomID); getTargetUser(data, action.target); @@ -323,7 +339,7 @@ PubSub::PubSub() return; } } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } action.modded = true; @@ -331,7 +347,8 @@ PubSub::PubSub() this->signals_.moderation.moderationStateChanged.invoke(action); }; - this->moderationActionHandlers["timeout"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["timeout"] = [this](const auto &data, + const auto &roomID) { BanAction action(data, roomID); getCreatedByUser(data, action.source); @@ -363,11 +380,12 @@ PubSub::PubSub() this->signals_.moderation.userBanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; - this->moderationActionHandlers["ban"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["ban"] = [this](const auto &data, + const auto &roomID) { BanAction action(data, roomID); getCreatedByUser(data, action.source); @@ -392,11 +410,12 @@ PubSub::PubSub() this->signals_.moderation.userBanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; - this->moderationActionHandlers["unban"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["unban"] = [this](const auto &data, + const auto &roomID) { UnbanAction action(data, roomID); getCreatedByUser(data, action.source); @@ -417,11 +436,12 @@ PubSub::PubSub() this->signals_.moderation.userUnbanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; - this->moderationActionHandlers["untimeout"] = [this](const auto &data, const auto &roomID) { + this->moderationActionHandlers["untimeout"] = [this](const auto &data, + const auto &roomID) { UnbanAction action(data, roomID); getCreatedByUser(data, action.source); @@ -442,21 +462,26 @@ PubSub::PubSub() this->signals_.moderation.userUnbanned.invoke(action); } catch (const std::runtime_error &ex) { - Log("Error parsing moderation action: {}", ex.what()); + log("Error parsing moderation action: {}", ex.what()); } }; this->websocketClient.set_access_channels(websocketpp::log::alevel::all); - this->websocketClient.clear_access_channels(websocketpp::log::alevel::frame_payload); + this->websocketClient.clear_access_channels( + websocketpp::log::alevel::frame_payload); this->websocketClient.init_asio(); // SSL Handshake - this->websocketClient.set_tls_init_handler(bind(&PubSub::onTLSInit, this, ::_1)); + this->websocketClient.set_tls_init_handler( + bind(&PubSub::onTLSInit, this, ::_1)); - this->websocketClient.set_message_handler(bind(&PubSub::onMessage, this, ::_1, ::_2)); - this->websocketClient.set_open_handler(bind(&PubSub::onConnectionOpen, this, ::_1)); - this->websocketClient.set_close_handler(bind(&PubSub::onConnectionClose, this, ::_1)); + this->websocketClient.set_message_handler( + bind(&PubSub::onMessage, this, ::_1, ::_2)); + this->websocketClient.set_open_handler( + bind(&PubSub::onConnectionOpen, this, ::_1)); + this->websocketClient.set_close_handler( + bind(&PubSub::onConnectionClose, this, ::_1)); // Add an initial client this->addClient(); @@ -468,7 +493,7 @@ void PubSub::addClient() auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, ec); if (ec) { - Log("Unable to establish connection: {}", ec.message()); + log("Unable to establish connection: {}", ec.message()); return; } @@ -477,7 +502,8 @@ void PubSub::addClient() void PubSub::start() { - this->mainThread.reset(new std::thread(std::bind(&PubSub::runThread, this))); + this->mainThread.reset( + new std::thread(std::bind(&PubSub::runThread, this))); } void PubSub::listenToWhispers(std::shared_ptr account) @@ -486,7 +512,7 @@ void PubSub::listenToWhispers(std::shared_ptr account) std::string userID = account->getUserId().toStdString(); - Log("Connection open!"); + log("Connection open!"); websocketpp::lib::error_code ec; std::vector topics({"whispers." + userID}); @@ -494,7 +520,7 @@ void PubSub::listenToWhispers(std::shared_ptr account) this->listen(createListenMessage(topics, account)); if (ec) { - Log("Unable to send message to websocket server: {}", ec.message()); + log("Unable to send message to websocket server: {}", ec.message()); return; } } @@ -507,27 +533,28 @@ void PubSub::unlistenAllModerationActions() } } -void PubSub::listenToChannelModerationActions(const QString &channelID, - std::shared_ptr account) +void PubSub::listenToChannelModerationActions( + const QString &channelID, std::shared_ptr account) { assert(!channelID.isEmpty()); assert(account != nullptr); QString userID = account->getUserId(); - assert(!userID.isEmpty()); + if (userID.isEmpty()) return; std::string topic(fS("chat_moderator_actions.{}.{}", userID, channelID)); if (this->isListeningToTopic(topic)) { - Log("We are already listening to topic {}", topic); + log("We are already listening to topic {}", topic); return; } - Log("Listen to topic {}", topic); + log("Listen to topic {}", topic); this->listenToTopic(topic, account); } -void PubSub::listenToTopic(const std::string &topic, std::shared_ptr account) +void PubSub::listenToTopic(const std::string &topic, + std::shared_ptr account) { auto message = createListenMessage({topic}, account); @@ -537,17 +564,18 @@ void PubSub::listenToTopic(const std::string &topic, std::shared_ptrtryListen(msg)) { - Log("Successfully listened!"); + log("Successfully listened!"); return; } - Log("Added to the back of the queue"); - this->requests.emplace_back(std::make_unique(std::move(msg))); + log("Added to the back of the queue"); + this->requests.emplace_back( + std::make_unique(std::move(msg))); } bool PubSub::tryListen(rapidjson::Document &msg) { - Log("tryListen with {} clients", this->clients.size()); + log("tryListen with {} clients", this->clients.size()); for (const auto &p : this->clients) { const auto &client = p.second; if (client->listen(msg)) { @@ -570,7 +598,8 @@ bool PubSub::isListeningToTopic(const std::string &topic) return false; } -void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr websocketMessage) +void PubSub::onMessage(websocketpp::connection_hdl hdl, + WebsocketMessagePtr websocketMessage) { const std::string &payload = websocketMessage->get_payload(); @@ -579,20 +608,22 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr webs rapidjson::ParseResult res = msg.Parse(payload.c_str()); if (!res) { - Log("Error parsing message '{}' from PubSub: {}", payload, + log("Error parsing message '{}' from PubSub: {}", payload, rapidjson::GetParseError_En(res.Code())); return; } if (!msg.IsObject()) { - Log("Error parsing message '{}' from PubSub. Root object is not an object", payload); + log("Error parsing message '{}' from PubSub. Root object is not an " + "object", + payload); return; } std::string type; if (!rj::getSafe(msg, "type", type)) { - Log("Missing required string member `type` in message root"); + log("Missing required string member `type` in message root"); return; } @@ -600,14 +631,14 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr webs this->handleListenResponse(msg); } else if (type == "MESSAGE") { if (!msg.HasMember("data")) { - Log("Missing required object member `data` in message root"); + log("Missing required object member `data` in message root"); return; } const auto &data = msg["data"]; if (!data.IsObject()) { - Log("Member `data` must be an object"); + log("Member `data` must be an object"); return; } @@ -615,23 +646,25 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr webs } else if (type == "PONG") { auto clientIt = this->clients.find(hdl); - // If this assert goes off, there's something wrong with the connection creation/preserving - // code KKona + // If this assert goes off, there's something wrong with the connection + // creation/preserving code KKona assert(clientIt != this->clients.end()); auto &client = *clientIt; client.second->handlePong(); } else { - Log("Unknown message type: {}", type); + log("Unknown message type: {}", type); } } void PubSub::onConnectionOpen(WebsocketHandle hdl) { - auto client = std::make_shared(this->websocketClient, hdl); + auto client = + std::make_shared(this->websocketClient, hdl); - // We separate the starting from the constructor because we will want to use shared_from_this + // We separate the starting from the constructor because we will want to use + // shared_from_this client->start(); this->clients.emplace(hdl, client); @@ -643,8 +676,8 @@ void PubSub::onConnectionClose(WebsocketHandle hdl) { auto clientIt = this->clients.find(hdl); - // If this assert goes off, there's something wrong with the connection creation/preserving - // code KKona + // If this assert goes off, there's something wrong with the connection + // creation/preserving code KKona assert(clientIt != this->clients.end()); auto &client = clientIt->second; @@ -658,14 +691,15 @@ void PubSub::onConnectionClose(WebsocketHandle hdl) PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl) { - WebsocketContextPtr ctx(new boost::asio::ssl::context(boost::asio::ssl::context::tlsv1)); + WebsocketContextPtr ctx( + new boost::asio::ssl::context(boost::asio::ssl::context::tlsv1)); try { ctx->set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::single_dh_use); } catch (const std::exception &e) { - Log("Exception caught in OnTLSInit: {}", e.what()); + log("Exception caught in OnTLSInit: {}", e.what()); } return ctx; @@ -680,12 +714,12 @@ void PubSub::handleListenResponse(const rapidjson::Document &msg) rj::getSafe(msg, "nonce", nonce); if (error.empty()) { - Log("Successfully listened to nonce {}", nonce); + log("Successfully listened to nonce {}", nonce); // Nothing went wrong return; } - Log("PubSub error: {} on nonce {}", error, nonce); + log("PubSub error: {} on nonce {}", error, nonce); return; } } @@ -695,14 +729,14 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) QString topic; if (!rj::getSafe(outerData, "topic", topic)) { - Log("Missing required string member `topic` in outerData"); + log("Missing required string member `topic` in outerData"); return; } std::string payload; if (!rj::getSafe(outerData, "message", payload)) { - Log("Expected string message in outerData"); + log("Expected string message in outerData"); return; } @@ -711,7 +745,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) rapidjson::ParseResult res = msg.Parse(payload.c_str()); if (!res) { - Log("Error parsing message '{}' from PubSub: {}", payload, + log("Error parsing message '{}' from PubSub: {}", payload, rapidjson::GetParseError_En(res.Code())); return; } @@ -720,7 +754,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) std::string whisperType; if (!rj::getSafe(msg, "type", whisperType)) { - Log("Bad whisper data"); + log("Bad whisper data"); return; } @@ -731,7 +765,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) } else if (whisperType == "thread") { // Handle thread? } else { - Log("Invalid whisper type: {}", whisperType); + log("Invalid whisper type: {}", whisperType); assert(false); return; } @@ -743,30 +777,30 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData) std::string moderationAction; if (!rj::getSafe(data, "moderation_action", moderationAction)) { - Log("Missing moderation action in data: {}", rj::stringify(data)); + log("Missing moderation action in data: {}", rj::stringify(data)); return; } auto handlerIt = this->moderationActionHandlers.find(moderationAction); if (handlerIt == this->moderationActionHandlers.end()) { - Log("No handler found for moderation action {}", moderationAction); + log("No handler found for moderation action {}", moderationAction); return; } // Invoke handler function handlerIt->second(data, topicParts[2]); } else { - Log("Unknown topic: {}", topic); + log("Unknown topic: {}", topic); return; } } void PubSub::runThread() { - Log("Start pubsub manager thread"); + log("Start pubsub manager thread"); this->websocketClient.run(); - Log("Done with pubsub manager thread"); + log("Done with pubsub manager thread"); } } // namespace chatterino diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index 3d39b7efb..211502176 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -22,7 +22,8 @@ namespace chatterino { -using WebsocketClient = websocketpp::client; +using WebsocketClient = + websocketpp::client; using WebsocketHandle = websocketpp::connection_hdl; using WebsocketErrorCode = websocketpp::lib::error_code; @@ -71,11 +72,14 @@ private: class PubSub { - using WebsocketMessagePtr = websocketpp::config::asio_tls_client::message_type::ptr; - using WebsocketContextPtr = websocketpp::lib::shared_ptr; + using WebsocketMessagePtr = + websocketpp::config::asio_tls_client::message_type::ptr; + using WebsocketContextPtr = + websocketpp::lib::shared_ptr; template - using Signal = pajlada::Signals::Signal; // type-id is vector> + using Signal = + pajlada::Signals::Signal; // type-id is vector> WebsocketClient websocketClient; std::unique_ptr mainThread; @@ -121,13 +125,14 @@ public: void unlistenAllModerationActions(); - void listenToChannelModerationActions(const QString &channelID, - std::shared_ptr account); + void listenToChannelModerationActions( + const QString &channelID, std::shared_ptr account); std::vector> requests; private: - void listenToTopic(const std::string &topic, std::shared_ptr account); + void listenToTopic(const std::string &topic, + std::shared_ptr account); void listen(rapidjson::Document &&msg); bool tryListen(rapidjson::Document &msg); @@ -142,7 +147,8 @@ private: std::owner_less> clients; - std::unordered_map> + std::unordered_map> moderationActionHandlers; void onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr msg); diff --git a/src/providers/twitch/PubsubHelpers.cpp b/src/providers/twitch/PubsubHelpers.cpp index e93340fe4..cf4560f6b 100644 --- a/src/providers/twitch/PubsubHelpers.cpp +++ b/src/providers/twitch/PubsubHelpers.cpp @@ -31,8 +31,9 @@ bool getTargetUser(const rapidjson::Value &data, ActionUser &user) return rj::getSafe(data, "target_user_id", user.id); } -rapidjson::Document createListenMessage(const std::vector &topicsVec, - std::shared_ptr account) +rapidjson::Document createListenMessage( + const std::vector &topicsVec, + std::shared_ptr account) { rapidjson::Document msg(rapidjson::kObjectType); auto &a = msg.GetAllocator(); @@ -57,7 +58,8 @@ rapidjson::Document createListenMessage(const std::vector &topicsVe return msg; } -rapidjson::Document createUnlistenMessage(const std::vector &topicsVec) +rapidjson::Document createUnlistenMessage( + const std::vector &topicsVec) { rapidjson::Document msg(rapidjson::kObjectType); auto &a = msg.GetAllocator(); diff --git a/src/providers/twitch/PubsubHelpers.hpp b/src/providers/twitch/PubsubHelpers.hpp index 07bda24b1..8b9b3f322 100644 --- a/src/providers/twitch/PubsubHelpers.hpp +++ b/src/providers/twitch/PubsubHelpers.hpp @@ -19,20 +19,23 @@ bool getCreatedByUser(const rapidjson::Value &data, ActionUser &user); bool getTargetUser(const rapidjson::Value &data, ActionUser &user); -rapidjson::Document createListenMessage(const std::vector &topicsVec, - std::shared_ptr account); -rapidjson::Document createUnlistenMessage(const std::vector &topicsVec); +rapidjson::Document createListenMessage( + const std::vector &topicsVec, + std::shared_ptr account); +rapidjson::Document createUnlistenMessage( + const std::vector &topicsVec); // Create timer using given ioService template -void runAfter(boost::asio::io_service &ioService, Duration duration, Callback cb) +void runAfter(boost::asio::io_service &ioService, Duration duration, + Callback cb) { auto timer = std::make_shared(ioService); timer->expires_from_now(duration); timer->async_wait([timer, cb](const boost::system::error_code &ec) { if (ec) { - Log("Error in runAfter: {}", ec.message()); + log("Error in runAfter: {}", ec.message()); return; } @@ -42,13 +45,14 @@ void runAfter(boost::asio::io_service &ioService, Duration duration, Callback cb // Use provided timer template -void runAfter(std::shared_ptr timer, Duration duration, Callback cb) +void runAfter(std::shared_ptr timer, + Duration duration, Callback cb) { timer->expires_from_now(duration); timer->async_wait([timer, cb](const boost::system::error_code &ec) { if (ec) { - Log("Error in runAfter: {}", ec.message()); + log("Error in runAfter: {}", ec.message()); return; } diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 785f159e5..0258c6e72 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -1,14 +1,47 @@ #include "providers/twitch/TwitchAccount.hpp" +#include + +#include "Application.hpp" #include "common/NetworkRequest.hpp" -#include "common/UrlFetch.hpp" #include "debug/Log.hpp" #include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchCommon.hpp" +#include "singletons/Emotes.hpp" #include "util/RapidjsonHelpers.hpp" namespace chatterino { +namespace { + +EmoteName cleanUpCode(const EmoteName &dirtyEmoteCode) +{ + auto cleanCode = dirtyEmoteCode.string; + cleanCode.detach(); + + static QMap emoteNameReplacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, + {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, + {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, + {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, + {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, + }; + + auto it = emoteNameReplacements.find(dirtyEmoteCode.string); + if (it != emoteNameReplacements.end()) { + cleanCode = it.value(); + } + + cleanCode.replace("<", "<"); + cleanCode.replace(">", ">"); + + return {cleanCode}; +} + +} // namespace + TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken, const QString &oauthClient, const QString &userID) : Account(ProviderId::Twitch) @@ -45,6 +78,16 @@ const QString &TwitchAccount::getUserId() const return this->userId_; } +QColor TwitchAccount::color() +{ + return this->color_.get(); +} + +void TwitchAccount::setColor(QColor color) +{ + this->color_.set(color); +} + bool TwitchAccount::setOAuthClient(const QString &newClientID) { if (this->oauthClient_.compare(newClientID) == 0) { @@ -74,25 +117,26 @@ bool TwitchAccount::isAnon() const void TwitchAccount::loadIgnores() { - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks"); + QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/blocks"); NetworkRequest req(url); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); if (!document.IsObject()) { - return false; + return Failure; } auto blocksIt = document.FindMember("blocks"); if (blocksIt == document.MemberEnd()) { - return false; + return Failure; } const auto &blocks = blocksIt->value; if (!blocks.IsArray()) { - return false; + return Failure; } { @@ -109,7 +153,8 @@ void TwitchAccount::loadIgnores() } TwitchUser ignoredUser; if (!rj::getSafe(userIt->value, ignoredUser)) { - Log("Error parsing twitch user JSON {}", rj::stringify(userIt->value)); + log("Error parsing twitch user JSON {}", + rj::stringify(userIt->value)); continue; } @@ -117,58 +162,64 @@ void TwitchAccount::loadIgnores() } } - return true; + return Success; }); req.execute(); } -void TwitchAccount::ignore(const QString &targetName, - std::function onFinished) +void TwitchAccount::ignore( + const QString &targetName, + std::function onFinished) { - const auto onIdFetched = [this, targetName, onFinished](QString targetUserId) { + const auto onIdFetched = [this, targetName, + onFinished](QString targetUserId) { this->ignoreByID(targetUserId, targetName, onFinished); // }; - PartialTwitchUser::byName(this->userName_).getId(onIdFetched); + PartialTwitchUser::byName(targetName).getId(onIdFetched); } -void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targetName, - std::function onFinished) +void TwitchAccount::ignoreByID( + const QString &targetUserID, const QString &targetName, + std::function onFinished) { - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks/" + - targetUserID); - + QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/blocks/" + targetUserID); NetworkRequest req(url, NetworkRequestType::Put); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); req.onError([=](int errorCode) { - onFinished(IgnoreResult_Failed, "An unknown error occured while trying to ignore user " + - targetName + " (" + QString::number(errorCode) + ")"); + onFinished(IgnoreResult_Failed, + "An unknown error occured while trying to ignore user " + + targetName + " (" + QString::number(errorCode) + ")"); return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); if (!document.IsObject()) { - onFinished(IgnoreResult_Failed, "Bad JSON data while ignoring user " + targetName); - return false; + onFinished(IgnoreResult_Failed, + "Bad JSON data while ignoring user " + targetName); + return Failure; } auto userIt = document.FindMember("user"); if (userIt == document.MemberEnd()) { onFinished(IgnoreResult_Failed, - "Bad JSON data while ignoring user (missing user) " + targetName); - return false; + "Bad JSON data while ignoring user (missing user) " + + targetName); + return Failure; } TwitchUser ignoredUser; if (!rj::getSafe(userIt->value, ignoredUser)) { onFinished(IgnoreResult_Failed, - "Bad JSON data while ignoring user (invalid user) " + targetName); - return false; + "Bad JSON data while ignoring user (invalid user) " + + targetName); + return Failure; } { std::lock_guard lock(this->ignoresMutex_); @@ -179,33 +230,36 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe existingUser.update(ignoredUser); onFinished(IgnoreResult_AlreadyIgnored, "User " + targetName + " is already ignored"); - return false; + return Failure; } } - onFinished(IgnoreResult_Success, "Successfully ignored user " + targetName); + onFinished(IgnoreResult_Success, + "Successfully ignored user " + targetName); - return true; + return Success; }); req.execute(); } -void TwitchAccount::unignore(const QString &targetName, - std::function onFinished) +void TwitchAccount::unignore( + const QString &targetName, + std::function onFinished) { - const auto onIdFetched = [this, targetName, onFinished](QString targetUserId) { + const auto onIdFetched = [this, targetName, + onFinished](QString targetUserId) { this->unignoreByID(targetUserId, targetName, onFinished); // }; - PartialTwitchUser::byName(this->userName_).getId(onIdFetched); + PartialTwitchUser::byName(targetName).getId(onIdFetched); } void TwitchAccount::unignoreByID( const QString &targetUserID, const QString &targetName, std::function onFinished) { - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks/" + - targetUserID); + QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/blocks/" + targetUserID); NetworkRequest req(url, NetworkRequestType::Delete); req.setCaller(QThread::currentThread()); @@ -213,13 +267,13 @@ void TwitchAccount::unignoreByID( req.onError([=](int errorCode) { onFinished(UnignoreResult_Failed, - "An unknown error occured while trying to unignore user " + targetName + " (" + - QString::number(errorCode) + ")"); + "An unknown error occured while trying to unignore user " + + targetName + " (" + QString::number(errorCode) + ")"); return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); TwitchUser ignoredUser; ignoredUser.id = targetUserID; @@ -228,9 +282,10 @@ void TwitchAccount::unignoreByID( this->ignores_.erase(ignoredUser); } - onFinished(UnignoreResult_Success, "Successfully unignored user " + targetName); + onFinished(UnignoreResult_Success, + "Successfully unignored user " + targetName); - return true; + return Success; }); req.execute(); @@ -239,8 +294,8 @@ 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); + QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/follows/channels/" + targetUserID); NetworkRequest req(url); req.setCaller(QThread::currentThread()); @@ -256,16 +311,17 @@ void TwitchAccount::checkFollow(const QString targetUserID, return true; }); - req.onSuccess([=](auto result) { + req.onSuccess([=](auto result) -> Outcome { auto document = result.parseRapidJson(); onFinished(FollowResult_Following); - return true; + return Success; }); req.execute(); } -void TwitchAccount::followUser(const QString userID, std::function successCallback) +void TwitchAccount::followUser(const QString userID, + std::function successCallback) { QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/follows/channels/" + userID); @@ -276,16 +332,17 @@ void TwitchAccount::followUser(const QString userID, std::function succe request.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); // TODO: Properly check result of follow request - request.onSuccess([successCallback](auto result) { + request.onSuccess([successCallback](auto result) -> Outcome { successCallback(); - return true; + return Success; }); request.execute(); } -void TwitchAccount::unfollowUser(const QString userID, std::function successCallback) +void TwitchAccount::unfollowUser(const QString userID, + std::function successCallback) { QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/follows/channels/" + userID); @@ -303,10 +360,10 @@ void TwitchAccount::unfollowUser(const QString userID, std::function suc return true; }); - request.onSuccess([successCallback](const auto &document) { + request.onSuccess([successCallback](const auto &document) -> Outcome { successCallback(); - return true; + return Success; }); request.execute(); @@ -319,26 +376,27 @@ std::set TwitchAccount::getIgnores() const return this->ignores_; } -void TwitchAccount::loadEmotes(std::function cb) +void TwitchAccount::loadEmotes() { - Log("Loading Twitch emotes for user {}", this->getUserName()); + log("Loading Twitch emotes for user {}", this->getUserName()); const auto &clientID = this->getOAuthClient(); const auto &oauthToken = this->getOAuthToken(); if (clientID.isEmpty() || oauthToken.isEmpty()) { - Log("Missing Client ID or OAuth token"); + log("Missing Client ID or OAuth token"); return; } - QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/emotes"); + QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/emotes"); NetworkRequest req(url); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); req.onError([=](int errorCode) { - Log("[TwitchAccount::loadEmotes] Error {}", errorCode); + log("[TwitchAccount::loadEmotes] Error {}", errorCode); if (errorCode == 203) { // onFinished(FollowResult_NotFollowing); } else { @@ -348,11 +406,131 @@ void TwitchAccount::loadEmotes(std::function return true; }); - req.onSuccess([=](auto result) { - cb(result.parseRapidJson()); + req.onSuccess([=](auto result) -> Outcome { + this->parseEmotes(result.parseRapidJson()); + + return Success; + }); + + req.execute(); +} + +AccessGuard +TwitchAccount::accessEmotes() const +{ + return this->emotes_.accessConst(); +} + +void TwitchAccount::parseEmotes(const rapidjson::Document &root) +{ + auto emoteData = this->emotes_.access(); + + emoteData->emoteSets.clear(); + emoteData->allEmoteNames.clear(); + + auto emoticonSets = root.FindMember("emoticon_sets"); + if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject()) { + log("No emoticon_sets in load emotes response"); + return; + } + + for (const auto &emoteSetJSON : emoticonSets->value.GetObject()) { + auto emoteSet = std::make_shared(); + + emoteSet->key = emoteSetJSON.name.GetString(); + + this->loadEmoteSetData(emoteSet); + + for (const rapidjson::Value &emoteJSON : + emoteSetJSON.value.GetArray()) { + if (!emoteJSON.IsObject()) { + log("Emote value was invalid"); + return; + } + + uint64_t idNumber; + if (!rj::getSafe(emoteJSON, "id", idNumber)) { + log("No ID key found in Emote value"); + return; + } + + QString _code; + if (!rj::getSafe(emoteJSON, "code", _code)) { + log("No code key found in Emote value"); + return; + } + + auto code = EmoteName{_code}; + auto id = EmoteId{QString::number(idNumber)}; + + auto cleanCode = cleanUpCode(code); + emoteSet->emotes.emplace_back(TwitchEmote{id, cleanCode}); + emoteData->allEmoteNames.push_back(cleanCode); + + auto emote = getApp()->emotes->twitch.getOrCreateEmote(id, code); + emoteData->emotes.emplace(code, emote); + } + + emoteData->emoteSets.emplace_back(emoteSet); + } +}; + +void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) +{ + if (!emoteSet) { + log("null emote set sent"); + return; + } + + auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); + if (staticSetIt != this->staticEmoteSets.end()) { + const auto &staticSet = staticSetIt->second; + emoteSet->channelName = staticSet.channelName; + emoteSet->text = staticSet.text; + return; + } + + NetworkRequest req( + "https://braize.pajlada.com/chatterino/twitchemotes/set/" + + emoteSet->key + "/"); + req.setUseQuickLoadCache(true); + + req.onError([](int errorCode) -> bool { + log("Error code {} while loading emote set data", errorCode); return true; }); + req.onSuccess([emoteSet](auto result) -> Outcome { + auto root = result.parseRapidJson(); + if (!root.IsObject()) { + return Failure; + } + + std::string emoteSetID; + QString channelName; + QString type; + if (!rj::getSafe(root, "channel_name", channelName)) { + return Failure; + } + + if (!rj::getSafe(root, "type", type)) { + return Failure; + } + + log("Loaded twitch emote set data for {}!", emoteSet->key); + + auto name = channelName; + name.detach(); + name[0] = name[0].toUpper(); + + emoteSet->text = name; + + emoteSet->type = type; + emoteSet->channelName = channelName; + + return Success; + }); + req.execute(); } diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index ee215209a..c59ca9d38 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -1,6 +1,9 @@ #pragma once +#include "common/Atomic.hpp" +#include "common/UniqueAccess.hpp" #include "controllers/accounts/Account.hpp" +#include "messages/Emote.hpp" #include "providers/twitch/TwitchUser.hpp" #include @@ -33,17 +36,42 @@ enum FollowResult { class TwitchAccount : public Account { public: - TwitchAccount(const QString &username, const QString &oauthToken_, const QString &oauthClient_, - const QString &_userID); + struct TwitchEmote { + EmoteId id; + EmoteName name; + }; + + struct EmoteSet { + QString key; + QString channelName; + QString text; + QString type; + std::vector emotes; + }; + + std::map staticEmoteSets; + + struct TwitchAccountEmoteData { + std::vector> emoteSets; + + std::vector allEmoteNames; + + EmoteMap emotes; + }; + + TwitchAccount(const QString &username, const QString &oauthToken_, + const QString &oauthClient_, const QString &_userID); virtual QString toString() const override; const QString &getUserName() const; const QString &getOAuthToken() const; const QString &getOAuthClient() const; - const QString &getUserId() const; + QColor color(); + void setColor(QColor color); + // Attempts to update the users OAuth Client ID // Returns true if the value has changed, otherwise false bool setOAuthClient(const QString &newClientID); @@ -57,32 +85,44 @@ public: void loadIgnores(); void ignore(const QString &targetName, std::function onFinished); - void ignoreByID(const QString &targetUserID, const QString &targetName, - std::function onFinished); - void unignore(const QString &targetName, - std::function onFinished); - void unignoreByID(const QString &targetUserID, const QString &targetName, - std::function onFinished); + void ignoreByID( + const QString &targetUserID, const QString &targetName, + std::function onFinished); + void unignore( + const QString &targetName, + std::function onFinished); + void unignoreByID( + const QString &targetUserID, const QString &targetName, + std::function onFinished); - void checkFollow(const QString targetUserID, std::function onFinished); - void followUser(const QString userID, std::function successCallback); - void unfollowUser(const QString userID, std::function successCallback); + void checkFollow(const QString targetUserID, + std::function onFinished); + void followUser(const QString userID, + std::function successCallback); + void unfollowUser(const QString userID, + std::function successCallback); std::set getIgnores() const; - void loadEmotes(std::function cb); - - QColor color; + void loadEmotes(); + AccessGuard accessEmotes() const; private: + void parseEmotes(const rapidjson::Document &document); + void loadEmoteSetData(std::shared_ptr emoteSet); + QString oauthClient_; QString oauthToken_; QString userName_; QString userId_; const bool isAnon_; + Atomic color_; mutable std::mutex ignoresMutex_; std::set ignores_; + + // std::map emotes; + UniqueAccess emotes_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index 2f177a146..18210c2fd 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -72,16 +72,17 @@ void TwitchAccountManager::reloadUsers() continue; } - std::string username = - pajlada::Settings::Setting::get("/accounts/" + uid + "/username"); - std::string userID = - pajlada::Settings::Setting::get("/accounts/" + uid + "/userID"); - std::string clientID = - pajlada::Settings::Setting::get("/accounts/" + uid + "/clientID"); - std::string oauthToken = - pajlada::Settings::Setting::get("/accounts/" + uid + "/oauthToken"); + std::string username = pajlada::Settings::Setting::get( + "/accounts/" + uid + "/username"); + std::string userID = pajlada::Settings::Setting::get( + "/accounts/" + uid + "/userID"); + std::string clientID = pajlada::Settings::Setting::get( + "/accounts/" + uid + "/clientID"); + std::string oauthToken = pajlada::Settings::Setting::get( + "/accounts/" + uid + "/oauthToken"); - if (username.empty() || userID.empty() || clientID.empty() || oauthToken.empty()) { + if (username.empty() || userID.empty() || clientID.empty() || + oauthToken.empty()) { continue; } @@ -92,18 +93,20 @@ void TwitchAccountManager::reloadUsers() switch (this->addUser(userData)) { case AddUserResponse::UserAlreadyExists: { - Log("User {} already exists", userData.username); + log("User {} already exists", userData.username); // Do nothing } break; case AddUserResponse::UserValuesUpdated: { - Log("User {} already exists, and values updated!", userData.username); + log("User {} already exists, and values updated!", + userData.username); if (userData.username == this->getCurrent()->getUserName()) { - Log("It was the current user, so we need to reconnect stuff!"); + log("It was the current user, so we need to reconnect " + "stuff!"); this->currentUserChanged.invoke(); } } break; case AddUserResponse::UserAdded: { - Log("Added user {}", userData.username); + log("Added user {}", userData.username); listUpdated = true; } break; } @@ -122,11 +125,13 @@ void TwitchAccountManager::load() QString newUsername(QString::fromStdString(newValue)); auto user = this->findUserByUsername(newUsername); if (user) { - Log("[AccountManager:currentUsernameChanged] User successfully updated to {}", + log("[AccountManager:currentUsernameChanged] User successfully " + "updated to {}", newUsername); this->currentUser_ = user; } else { - Log("[AccountManager:currentUsernameChanged] User successfully updated to anonymous"); + log("[AccountManager:currentUsernameChanged] User successfully " + "updated to anonymous"); this->currentUser_ = this->anonymousUser_; } @@ -140,8 +145,8 @@ bool TwitchAccountManager::isLoggedIn() const return false; } - // Once `TwitchAccount` class has a way to check, we should also return false if the credentials - // are incorrect + // Once `TwitchAccount` class has a way to check, we should also return + // false if the credentials are incorrect return !this->currentUser_->isAnon(); } @@ -151,11 +156,13 @@ bool TwitchAccountManager::removeUser(TwitchAccount *account) std::string userID(account->getUserId().toStdString()); if (!userID.empty()) { - pajlada::Settings::SettingManager::removeSetting("/accounts/uid" + userID); + pajlada::Settings::SettingManager::removeSetting("/accounts/uid" + + userID); } if (account->getUserName() == qS(this->currentUsername.getValue())) { - // The user that was removed is the current user, log into the anonymous user + // The user that was removed is the current user, log into the anonymous + // user this->currentUsername = ""; } @@ -186,8 +193,9 @@ TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser( } } - auto newUser = std::make_shared(userData.username, userData.oauthToken, - userData.clientID, userData.userID); + auto newUser = + std::make_shared(userData.username, userData.oauthToken, + userData.clientID, userData.userID); // std::lock_guard lock(this->mutex); diff --git a/src/providers/twitch/TwitchAccountManager.hpp b/src/providers/twitch/TwitchAccountManager.hpp index 1cb1df1c2..32961f092 100644 --- a/src/providers/twitch/TwitchAccountManager.hpp +++ b/src/providers/twitch/TwitchAccountManager.hpp @@ -11,7 +11,8 @@ // // Warning: This class is not supposed to be created directly. -// Get yourself an instance from our friends over at AccountManager.hpp +// Get yourself an instance from our friends over at +// AccountManager.hpp // namespace chatterino { @@ -30,12 +31,14 @@ public: QString oauthToken; }; - // Returns the current twitchUsers, or the anonymous user if we're not currently logged in + // Returns the current twitchUsers, or the anonymous user if we're not + // currently logged in std::shared_ptr getCurrent(); std::vector getUsernames() const; - std::shared_ptr findUserByUsername(const QString &username) const; + std::shared_ptr findUserByUsername( + const QString &username) const; bool userExists(const QString &username) const; void reloadUsers(); @@ -43,11 +46,13 @@ public: bool isLoggedIn() const; - pajlada::Settings::Setting currentUsername = {"/accounts/current", ""}; + pajlada::Settings::Setting currentUsername = { + "/accounts/current", ""}; pajlada::Signals::NoArgSignal currentUserChanged; pajlada::Signals::NoArgSignal userListUpdated; - SortedSignalVector, SharedPtrElementLess> + SortedSignalVector, + SharedPtrElementLess> accounts; private: diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp new file mode 100644 index 000000000..dc0329b69 --- /dev/null +++ b/src/providers/twitch/TwitchApi.cpp @@ -0,0 +1,54 @@ +#include "providers/twitch/TwitchApi.hpp" + +#include "common/NetworkRequest.hpp" +#include "debug/Log.hpp" +#include "providers/twitch/TwitchCommon.hpp" + +#include + +namespace chatterino { + +void TwitchApi::findUserId(const QString user, + std::function successCallback) +{ + QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user); + + NetworkRequest request(requestUrl); + request.setCaller(QThread::currentThread()); + request.makeAuthorizedV5(getDefaultClientID()); + request.setTimeout(30000); + request.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 id = firstUser.value("_id"); + if (!id.isString()) { + log("API Error: while getting user id, first user object `_id` key " + "is not a " + "string"); + successCallback(""); + return Failure; + } + successCallback(id.toString()); + return Success; + }); + + request.execute(); +} + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchApi.hpp b/src/providers/twitch/TwitchApi.hpp new file mode 100644 index 000000000..67b26c1ca --- /dev/null +++ b/src/providers/twitch/TwitchApi.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace chatterino { + +class TwitchApi +{ +public: + static void findUserId(const QString user, + std::function callback); + +private: +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp new file mode 100644 index 000000000..fd9fe7461 --- /dev/null +++ b/src/providers/twitch/TwitchBadges.cpp @@ -0,0 +1,64 @@ +#include "TwitchBadges.hpp" + +#include +#include +#include +#include +#include "common/NetworkRequest.hpp" + +namespace chatterino { + +TwitchBadges::TwitchBadges() +{ +} + +void TwitchBadges::initialize(Settings &settings, Paths &paths) +{ + this->loadTwitchBadges(); +} + +void TwitchBadges::loadTwitchBadges() +{ + static QString url( + "https://badges.twitch.tv/v1/badges/global/display?language=en"); + + NetworkRequest req(url); + req.setCaller(QThread::currentThread()); + req.onSuccess([this](auto result) -> Outcome { + auto root = result.parseJson(); + QJsonObject sets = root.value("badge_sets").toObject(); + + for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { + QJsonObject versions = + it.value().toObject().value("versions").toObject(); + + for (auto versionIt = std::begin(versions); + versionIt != std::end(versions); ++versionIt) { + auto emote = Emote{ + {""}, + ImageSet{ + Image::fromUrl({root.value("image_url_1x").toString()}, + 1), + Image::fromUrl({root.value("image_url_2x").toString()}, + 0.5), + Image::fromUrl({root.value("image_url_4x").toString()}, + 0.25), + }, + Tooltip{root.value("description").toString()}, + Url{root.value("clickURL").toString()}}; + // "title" + // "clickAction" + + QJsonObject versionObj = versionIt.value().toObject(); + this->badges.emplace(versionIt.key(), + std::make_shared(emote)); + } + } + + return Success; + }); + + req.execute(); +} + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.hpp b/src/providers/twitch/TwitchBadges.hpp new file mode 100644 index 000000000..a228365da --- /dev/null +++ b/src/providers/twitch/TwitchBadges.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include "util/QStringHash.hpp" + +namespace chatterino { + +class Settings; +class Paths; + +class TwitchBadges +{ +public: + TwitchBadges(); + + void initialize(Settings &settings, Paths &paths); + +private: + void loadTwitchBadges(); + + std::unordered_map badges; +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 61de45318..7e0a1efc1 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1,126 +1,110 @@ #include "providers/twitch/TwitchChannel.hpp" #include "common/Common.hpp" -#include "common/UrlFetch.hpp" +#include "common/NetworkRequest.hpp" #include "controllers/accounts/AccountController.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" +#include "providers/bttv/BttvEmotes.hpp" +#include "providers/bttv/LoadBttvChannelEmote.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" +#include "providers/twitch/TwitchParseCheerEmotes.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "util/PostToThread.hpp" #include +#include +#include +#include #include #include namespace chatterino { - -TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection *readConnection) - : Channel(channelName, Channel::Type::Twitch) - , bttvChannelEmotes(new EmoteMap) - , ffzChannelEmotes(new EmoteMap) - , subscriptionURL("https://www.twitch.tv/subs/" + name) - , channelURL("https://twitch.tv/" + name) - , popoutPlayerURL("https://player.twitch.tv/?channel=" + name) - , mod_(false) - , readConnection_(readConnection) +namespace { +auto parseRecentMessages(const QJsonObject &jsonRoot, TwitchChannel &channel) { - Log("[TwitchChannel:{}] Opened", this->name); + QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); + std::vector messages; - this->startRefreshLiveStatusTimer(60 * 1000); + if (jsonMessages.empty()) return messages; - auto app = getApp(); - this->reloadChannelEmotes(); + for (const auto jsonMessage : jsonMessages) { + auto content = jsonMessage.toString().toUtf8(); + // passing nullptr as the channel makes the message invalid but we don't + // check for that anyways + auto message = Communi::IrcMessage::fromData(content, nullptr); + auto privMsg = dynamic_cast(message); + assert(privMsg); - this->managedConnect(app->accounts->twitch.currentUserChanged, - [this]() { this->setMod(false); }); - - auto refreshPubSubState = [=]() { - if (!this->hasModRights()) { - return; + MessageParseArgs args; + TwitchMessageBuilder builder(&channel, privMsg, args); + if (!builder.isIgnored()) { + messages.push_back(builder.build()); } + } - if (this->roomID.isEmpty()) { - return; - } + return messages; +} +} // namespace - auto account = app->accounts->twitch.getCurrent(); - if (account && !account->getUserId().isEmpty()) { - app->twitch.pubsub->listenToChannelModerationActions(this->roomID, account); - } - }; +TwitchChannel::TwitchChannel(const QString &name) + : Channel(name, Channel::Type::Twitch) + , bttvEmotes_(std::make_shared()) + , ffzEmotes_(std::make_shared()) + , subscriptionUrl_("https://www.twitch.tv/subs/" + name) + , channelUrl_("https://twitch.tv/" + name) + , popoutPlayerUrl_("https://player.twitch.tv/?channel=" + name) + , mod_(false) +{ + log("[TwitchChannel:{}] Opened", name); - this->userStateChanged.connect(refreshPubSubState); - this->roomIDchanged.connect(refreshPubSubState); - this->managedConnect(app->accounts->twitch.currentUserChanged, refreshPubSubState); - refreshPubSubState(); + // this->refreshChannelEmotes(); + // this->refreshViewerList(); - this->fetchMessages.connect([this] { - this->fetchRecentMessages(); // + this->managedConnect(getApp()->accounts->twitch.currentUserChanged, + [=] { this->setMod(false); }); + + // pubsub + this->userStateChanged.connect([=] { this->refreshPubsub(); }); + this->managedConnect(getApp()->accounts->twitch.currentUserChanged, + [=] { this->refreshPubsub(); }); + this->refreshPubsub(); + + // room id loaded -> refresh live status + this->roomIdChanged.connect([this]() { + this->refreshPubsub(); + this->refreshLiveStatus(); + this->loadBadges(); + this->loadCheerEmotes(); }); + // timers + QObject::connect(&this->chattersListTimer_, &QTimer::timeout, + [=] { this->refreshViewerList(); }); + this->chattersListTimer_.start(5 * 60 * 1000); + + QObject::connect(&this->liveStatusTimer_, &QTimer::timeout, + [=] { this->refreshLiveStatus(); }); + this->liveStatusTimer_.start(60 * 1000); + + // -- this->messageSuffix_.append(' '); this->messageSuffix_.append(QChar(0x206D)); - static QStringList jsonLabels = {"moderators", "staff", "admins", "global_mods", "viewers"}; - auto refreshChatters = [=](QJsonObject obj) { - QJsonObject chattersObj = obj.value("chatters").toObject(); - for (int i = 0; i < jsonLabels.size(); i++) { - foreach (const QJsonValue &v, chattersObj.value(jsonLabels.at(i)).toArray()) { - this->completionModel.addUser(v.toString()); - } - } - }; - - auto doRefreshChatters = [=]() { - const auto streamStatus = this->getStreamStatus(); - - if (app->settings->onlyFetchChattersForSmallerStreamers) { - if (streamStatus.live && streamStatus.viewerCount > app->settings->smallStreamerLimit) { - return; - } - } - - NetworkRequest request("https://tmi.twitch.tv/group/user/" + this->name + "/chatters"); - - request.setCaller(QThread::currentThread()); - request.onSuccess([refreshChatters](auto result) { - refreshChatters(result.parseJson()); // - - return true; - }); - - request.execute(); - }; - - doRefreshChatters(); - - this->chattersListTimer = new QTimer; - QObject::connect(this->chattersListTimer, &QTimer::timeout, doRefreshChatters); - this->chattersListTimer->start(5 * 60 * 1000); - + // debugging #if 0 for (int i = 0; i < 1000; i++) { - this->addMessage(Message::createSystemMessage("asdf")); + this->addMessage(makeSystemMessage("asdf")); } #endif } -TwitchChannel::~TwitchChannel() -{ - this->liveStatusTimer->stop(); - this->liveStatusTimer->deleteLater(); - - this->chattersListTimer->stop(); - this->chattersListTimer->deleteLater(); -} - bool TwitchChannel::isEmpty() const { - return this->name.isEmpty(); + return this->getName().isEmpty(); } bool TwitchChannel::canSendMessage() const @@ -128,21 +112,20 @@ bool TwitchChannel::canSendMessage() const return !this->isEmpty(); } -void TwitchChannel::setRoomID(const QString &_roomID) +void TwitchChannel::refreshChannelEmotes() { - this->roomID = _roomID; - this->roomIDchanged.invoke(); - this->fetchMessages.invoke(); -} - -void TwitchChannel::reloadChannelEmotes() -{ - auto app = getApp(); - - Log("[TwitchChannel:{}] Reloading channel emotes", this->name); - - app->emotes->bttv.loadChannelEmotes(this->name, this->bttvChannelEmotes); - app->emotes->ffz.loadChannelEmotes(this->name, this->ffzChannelEmotes); + BttvEmotes::loadChannel( + this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) + this->bttvEmotes_.set( + std::make_shared(std::move(emoteMap))); + }); + FfzEmotes::loadChannel( + this->getName(), [this, weak = weakOf(this)](auto &&emoteMap) { + if (auto shared = weak.lock()) + this->ffzEmotes_.set( + std::make_shared(std::move(emoteMap))); + }); } void TwitchChannel::sendMessage(const QString &message) @@ -150,15 +133,15 @@ void TwitchChannel::sendMessage(const QString &message) auto app = getApp(); if (!app->accounts->twitch.isLoggedIn()) { - // XXX: It would be nice if we could add a link here somehow that opened the "account - // manager" dialog + // XXX: It would be nice if we could add a link here somehow that opened + // the "account manager" dialog this->addMessage( - Message::createSystemMessage("You need to log in to send messages. You can " - "link your Twitch account in the settings.")); + makeSystemMessage("You need to log in to send messages. You can " + "link your Twitch account in the settings.")); return; } - Log("[TwitchChannel:{}] Send message: {}", this->name, message); + log("[TwitchChannel:{}] Send message: {}", this->getName(), message); // Do last message processing QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); @@ -170,7 +153,7 @@ void TwitchChannel::sendMessage(const QString &message) } if (!this->hasModRights()) { - if (app->settings->allowDuplicateMessages) { + if (getSettings()->allowDuplicateMessages) { if (parsedMessage == this->lastSentMessage_) { parsedMessage.append(this->messageSuffix_); } @@ -178,7 +161,7 @@ void TwitchChannel::sendMessage(const QString &message) } bool messageSent = false; - this->sendMessageSignal.invoke(this->name, parsedMessage, messageSent); + this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); if (messageSent) { qDebug() << "sent"; @@ -204,43 +187,38 @@ bool TwitchChannel::isBroadcaster() const { auto app = getApp(); - return this->name == app->accounts->twitch.getCurrent()->getUserName(); + return this->getName() == app->accounts->twitch.getCurrent()->getUserName(); } -void TwitchChannel::addRecentChatter(const std::shared_ptr &message) +void TwitchChannel::addRecentChatter(const MessagePtr &message) { assert(!message->loginName.isEmpty()); - std::lock_guard lock(this->recentChattersMutex_); - - this->recentChatters_[message->loginName] = {message->displayName, message->localizedName}; - this->completionModel.addUser(message->displayName); } void TwitchChannel::addJoinedUser(const QString &user) { - auto *app = getApp(); + auto app = getApp(); if (user == app->accounts->twitch.getCurrent()->getUserName() || - !app->settings->showJoins.getValue()) { + !getSettings()->showJoins.getValue()) { return; } - std::lock_guard guard(this->joinedUserMutex_); - - joinedUsers_ << user; + auto joinedUsers = this->joinedUsers_.access(); + joinedUsers->append(user); if (!this->joinedUsersMergeQueued_) { this->joinedUsersMergeQueued_ = true; - QTimer::singleShot(500, &this->object_, [this] { - std::lock_guard guard(this->joinedUserMutex_); + QTimer::singleShot(500, &this->lifetimeGuard_, [this] { + auto joinedUsers = this->joinedUsers_.access(); - auto message = - Message::createSystemMessage("Users joined: " + this->joinedUsers_.join(", ")); - message->flags |= Message::Collapsed; - this->addMessage(message); - this->joinedUsers_.clear(); + MessageBuilder builder(systemMessage, + "Users joined: " + joinedUsers->join(", ")); + builder->flags.set(MessageFlag::Collapsed); + joinedUsers->clear(); + this->addMessage(builder.release()); this->joinedUsersMergeQueued_ = false; }); } @@ -248,242 +226,442 @@ void TwitchChannel::addJoinedUser(const QString &user) void TwitchChannel::addPartedUser(const QString &user) { - auto *app = getApp(); + auto app = getApp(); if (user == app->accounts->twitch.getCurrent()->getUserName() || - !app->settings->showJoins.getValue()) { + !getSettings()->showJoins.getValue()) { return; } - std::lock_guard guard(this->partedUserMutex_); - - partedUsers_ << user; + auto partedUsers = this->partedUsers_.access(); + partedUsers->append(user); if (!this->partedUsersMergeQueued_) { this->partedUsersMergeQueued_ = true; - QTimer::singleShot(500, &this->object_, [this] { - std::lock_guard guard(this->partedUserMutex_); + QTimer::singleShot(500, &this->lifetimeGuard_, [this] { + auto partedUsers = this->partedUsers_.access(); - auto message = - Message::createSystemMessage("Users parted: " + this->partedUsers_.join(", ")); - message->flags |= Message::Collapsed; - this->addMessage(message); - this->partedUsers_.clear(); + MessageBuilder builder(systemMessage, + "Users parted: " + partedUsers->join(", ")); + builder->flags.set(MessageFlag::Collapsed); + this->addMessage(builder.release()); + partedUsers->clear(); this->partedUsersMergeQueued_ = false; }); } } -TwitchChannel::RoomModes TwitchChannel::getRoomModes() +QString TwitchChannel::roomId() const { - std::lock_guard lock(this->roomModeMutex_); + return *this->roomID_.access(); +} - return this->roomModes_; +void TwitchChannel::setRoomId(const QString &id) +{ + (*this->roomID_.access()) = id; + this->roomIdChanged.invoke(); + this->loadRecentMessages(); +} + +AccessGuard TwitchChannel::accessRoomModes() + const +{ + return this->roomModes_.accessConst(); } void TwitchChannel::setRoomModes(const RoomModes &_roomModes) { - { - std::lock_guard lock(this->roomModeMutex_); - this->roomModes_ = _roomModes; - } + this->roomModes_ = _roomModes; this->roomModesChanged.invoke(); } bool TwitchChannel::isLive() const { - std::lock_guard lock(this->streamStatusMutex_); - return this->streamStatus_.live; + return this->streamStatus_.access()->live; } -TwitchChannel::StreamStatus TwitchChannel::getStreamStatus() const +AccessGuard +TwitchChannel::accessStreamStatus() const { - std::lock_guard lock(this->streamStatusMutex_); - return this->streamStatus_; + return this->streamStatus_.accessConst(); +} + +boost::optional TwitchChannel::bttvEmote(const EmoteName &name) const +{ + auto emotes = this->bttvEmotes_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +boost::optional TwitchChannel::ffzEmote(const EmoteName &name) const +{ + auto emotes = this->bttvEmotes_.get(); + auto it = emotes->find(name); + + if (it == emotes->end()) return boost::none; + return it->second; +} + +std::shared_ptr TwitchChannel::bttvEmotes() const +{ + return this->bttvEmotes_.get(); +} + +std::shared_ptr TwitchChannel::ffzEmotes() const +{ + return this->ffzEmotes_.get(); +} + +const QString &TwitchChannel::subscriptionUrl() +{ + return this->subscriptionUrl_; +} + +const QString &TwitchChannel::channelUrl() +{ + return this->channelUrl_; +} + +const QString &TwitchChannel::popoutPlayerUrl() +{ + return this->popoutPlayerUrl_; } void TwitchChannel::setLive(bool newLiveStatus) { bool gotNewLiveStatus = false; { - std::lock_guard lock(this->streamStatusMutex_); - if (this->streamStatus_.live != newLiveStatus) { + auto guard = this->streamStatus_.access(); + if (guard->live != newLiveStatus) { gotNewLiveStatus = true; - this->streamStatus_.live = newLiveStatus; + guard->live = newLiveStatus; } } if (gotNewLiveStatus) { - this->updateLiveInfo.invoke(); + this->liveStatusChanged.invoke(); } } void TwitchChannel::refreshLiveStatus() { - if (this->roomID.isEmpty()) { - Log("[TwitchChannel:{}] Refreshing live status (Missing ID)", this->name); + auto roomID = this->roomId(); + + if (roomID.isEmpty()) { + log("[TwitchChannel:{}] Refreshing live status (Missing ID)", + this->getName()); this->setLive(false); return; } - Log("[TwitchChannel:{}] Refreshing live status", this->name); + log("[TwitchChannel:{}] Refreshing live status", this->getName()); - QString url("https://api.twitch.tv/kraken/streams/" + this->roomID); + QString url("https://api.twitch.tv/kraken/streams/" + roomID); - std::weak_ptr weak = this->shared_from_this(); + //<<<<<<< HEAD + // auto request = makeGetStreamRequest(roomID, QThread::currentThread()); + //======= + auto request = NetworkRequest::twitchRequest(url); + request.setCaller(QThread::currentThread()); + //>>>>>>> 9bfbdefd2f0972a738230d5b95a009f73b1dd933 - auto request = makeGetStreamRequest(this->roomID, QThread::currentThread()); + request.onSuccess( + [this, weak = this->weak_from_this()](auto result) -> Outcome { + ChannelPtr shared = weak.lock(); + if (!shared) return Failure; - request.onSuccess([weak](auto result) { - auto d = result.parseRapidJson(); - ChannelPtr shared = weak.lock(); + return this->parseLiveStatus(result.parseRapidJson()); + }); - if (!shared) { - return false; + request.execute(); +} + +Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) +{ + if (!document.IsObject()) { + log("[TwitchChannel:refreshLiveStatus] root is not an object"); + return Failure; + } + + if (!document.HasMember("stream")) { + log("[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; + } + + if (!stream.HasMember("viewers") || !stream.HasMember("game") || + !stream.HasMember("channel") || !stream.HasMember("created_at")) { + log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); + this->setLive(false); + return Failure; + } + + const rapidjson::Value &streamChannel = stream["channel"]; + + if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) { + log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in " + "channel"); + return Failure; + } + + // Stream is live + + { + auto status = this->streamStatus_.access(); + status->live = true; + 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); + 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(); } - TwitchChannel *channel = dynamic_cast(shared.get()); + if (stream.HasMember("broadcast_platform")) { + const auto &broadcastPlatformValue = stream["broadcast_platform"]; - if (!d.IsObject()) { - Log("[TwitchChannel:refreshLiveStatus] root is not an object"); - return false; - } - - if (!d.HasMember("stream")) { - Log("[TwitchChannel:refreshLiveStatus] Missing stream in root"); - return false; - } - - const auto &stream = d["stream"]; - - if (!stream.IsObject()) { - // Stream is offline (stream is most likely null) - channel->setLive(false); - return false; - } - - if (!stream.HasMember("viewers") || !stream.HasMember("game") || - !stream.HasMember("channel") || !stream.HasMember("created_at")) { - Log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); - channel->setLive(false); - return false; - } - - const rapidjson::Value &streamChannel = stream["channel"]; - - if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) { - Log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in channel"); - return false; - } - - // Stream is live - - { - std::lock_guard lock(channel->streamStatusMutex_); - channel->streamStatus_.live = true; - channel->streamStatus_.viewerCount = stream["viewers"].GetUint(); - channel->streamStatus_.game = stream["game"].GetString(); - channel->streamStatus_.title = streamChannel["status"].GetString(); - QDateTime since = QDateTime::fromString(stream["created_at"].GetString(), Qt::ISODate); - auto diff = since.secsTo(QDateTime::currentDateTime()); - channel->streamStatus_.uptime = - QString::number(diff / 3600) + "h " + QString::number(diff % 3600 / 60) + "m"; - - channel->streamStatus_.rerun = false; - if (stream.HasMember("stream_type")) { - channel->streamStatus_.streamType = stream["stream_type"].GetString(); - } else { - channel->streamStatus_.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) { - channel->streamStatus_.rerun = true; - } + if (broadcastPlatformValue.IsString()) { + const char *broadcastPlatform = + stream["broadcast_platform"].GetString(); + if (strcmp(broadcastPlatform, "rerun") == 0) { + status->rerun = true; } } } + } - // Signal all listeners that the stream status has been updated - channel->updateLiveInfo.invoke(); + // Signal all listeners that the stream status has been updated + this->liveStatusChanged.invoke(); - return true; - }); - - request.execute(); + return Success; } -void TwitchChannel::startRefreshLiveStatusTimer(int intervalMS) -{ - this->liveStatusTimer = new QTimer; - QObject::connect(this->liveStatusTimer, &QTimer::timeout, [this]() { - this->refreshLiveStatus(); // - }); - - // When the Room ID of a twitch channel has been set, refresh the live status an extra time - this->roomIDchanged.connect([this]() { - this->refreshLiveStatus(); // - }); - - this->liveStatusTimer->start(intervalMS); -} - -void TwitchChannel::fetchRecentMessages() +void TwitchChannel::loadRecentMessages() { static QString genericURL = - "https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + getDefaultClientID(); + "https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + + getDefaultClientID(); - NetworkRequest request(genericURL.arg(this->roomID)); + NetworkRequest request(genericURL.arg(this->roomId())); request.makeAuthorizedV5(getDefaultClientID()); request.setCaller(QThread::currentThread()); + // can't be concurrent right now due to SignalVector + // request.setExecuteConcurrently(true); - std::weak_ptr weak = this->shared_from_this(); + request.onSuccess([that = this](auto result) -> Outcome { + auto messages = parseRecentMessages(result.parseJson(), *that); - request.onSuccess([weak](auto result) { - auto obj = result.parseJson(); - ChannelPtr shared = weak.lock(); + // postToThread([that, weak = weakOf(that), + // messages = std::move(messages)]() mutable { + that->addMessagesAtStart(messages); + // }); - if (!shared) { - return false; - } - - auto channel = dynamic_cast(shared.get()); - assert(channel != nullptr); - - static auto readConnection = channel->readConnection_; - - QJsonArray msgArray = obj.value("messages").toArray(); - if (msgArray.empty()) { - return false; - } - - std::vector messages; - - for (const QJsonValueRef _msg : msgArray) { - QByteArray content = _msg.toString().toUtf8(); - auto msg = Communi::IrcMessage::fromData(content, readConnection); - auto privMsg = static_cast(msg); - - MessageParseArgs args; - TwitchMessageBuilder builder(channel, privMsg, args); - if (!builder.isIgnored()) { - messages.push_back(builder.build()); - } - } - - channel->addMessagesAtStart(messages); - - return true; + return Success; }); request.execute(); } +void TwitchChannel::refreshPubsub() +{ + // listen to moderation actions + if (!this->hasModRights()) return; + auto roomId = this->roomId(); + if (roomId.isEmpty()) return; + + auto account = getApp()->accounts->twitch.getCurrent(); + getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId, + account); +} + +void TwitchChannel::refreshViewerList() +{ + // setting? + const auto streamStatus = this->accessStreamStatus(); + + if (getSettings()->onlyFetchChattersForSmallerStreamers) { + if (streamStatus->live && + streamStatus->viewerCount > getSettings()->smallStreamerLimit) { + return; + } + } + + // get viewer list + NetworkRequest request("https://tmi.twitch.tv/group/user/" + + this->getName() + "/chatters"); + + request.setCaller(QThread::currentThread()); + request.onSuccess( + [this, weak = this->weak_from_this()](auto result) -> Outcome { + // channel still exists? + auto shared = weak.lock(); + if (!shared) return Failure; + + return this->parseViewerList(result.parseJson()); + }); + + request.execute(); +} + +Outcome TwitchChannel::parseViewerList(const QJsonObject &jsonRoot) +{ + static QStringList categories = {"moderators", "staff", "admins", + "global_mods", "viewers"}; + + // parse json + QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); + + for (const auto &category : categories) { + for (const auto jsonCategory : + jsonCategories.value(category).toArray()) { + this->completionModel.addUser(jsonCategory.toString()); + } + } + + return Success; +} + +void TwitchChannel::loadBadges() +{ + auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" + + this->roomId() + "/display?language=en"}; + NetworkRequest req(url.string); + req.setCaller(QThread::currentThread()); + + req.onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { + auto shared = weak.lock(); + if (!shared) return Failure; + + auto badgeSets = this->badgeSets_.access(); + + auto jsonRoot = result.parseJson(); + + auto _ = jsonRoot["badge_sets"].toObject(); + for (auto jsonBadgeSet = _.begin(); jsonBadgeSet != _.end(); + jsonBadgeSet++) { + auto &versions = (*badgeSets)[jsonBadgeSet.key()]; + + auto _ = jsonBadgeSet->toObject()["versions"].toObject(); + for (auto jsonVersion_ = _.begin(); jsonVersion_ != _.end(); + jsonVersion_++) { + auto jsonVersion = jsonVersion_->toObject(); + auto emote = std::make_shared(Emote{ + EmoteName{}, + ImageSet{Image::fromUrl( + {jsonVersion["image_url_1x"].toString()}), + Image::fromUrl( + {jsonVersion["image_url_2x"].toString()}), + Image::fromUrl( + {jsonVersion["image_url_4x"].toString()})}, + Tooltip{jsonRoot["description"].toString()}, + Url{jsonVersion["clickURL"].toString()}}); + + versions.emplace(jsonVersion_.key(), emote); + }; + } + + return Success; + }); + + req.execute(); +} + +void TwitchChannel::loadCheerEmotes() +{ + /*auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" + + this->getRoomId()}; + auto request = NetworkRequest::twitchRequest(url.string); + request.setCaller(QThread::currentThread()); + + request.onSuccess( + [this, weak = weakOf(this)](auto result) -> Outcome { + auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson()); + std::vector emoteSets; + + for (auto &set : cheerEmoteSets) { + auto cheerEmoteSet = CheerEmoteSet(); + cheerEmoteSet.regex = QRegularExpression( + "^" + set.prefix.toLower() + "([1-9][0-9]*)$"); + + for (auto &tier : set.tiers) { + CheerEmote cheerEmote; + + cheerEmote.color = QColor(tier.color); + cheerEmote.minBits = tier.minBits; + + // TODO(pajlada): We currently hardcode dark here :| + // We will continue to do so for now since we haven't had to + // solve that anywhere else + + cheerEmote.animatedEmote = std::make_shared( + Emote{EmoteName{"cheer emote"}, + ImageSet{ + tier.images["dark"]["animated"]["1"], + tier.images["dark"]["animated"]["2"], + tier.images["dark"]["animated"]["4"], + }, + Tooltip{}, Url{}}); + cheerEmote.staticEmote = std::make_shared( + Emote{EmoteName{"cheer emote"}, + ImageSet{ + tier.images["dark"]["static"]["1"], + tier.images["dark"]["static"]["2"], + tier.images["dark"]["static"]["4"], + }, + Tooltip{}, Url{}}); + + cheerEmoteSet.cheerEmotes.emplace_back(cheerEmote); + } + + std::sort(cheerEmoteSet.cheerEmotes.begin(), + cheerEmoteSet.cheerEmotes.end(), + [](const auto &lhs, const auto &rhs) { + return lhs.minBits < rhs.minBits; // + }); + + emoteSets.emplace_back(cheerEmoteSet); + } + *this->cheerEmoteSets_.access() = std::move(emoteSets); + + return Success; + }); + + request.execute(); + */ +} + +boost::optional TwitchChannel::getTwitchBadge( + const QString &set, const QString &version) const +{ + auto badgeSets = this->badgeSets_.access(); + auto it = badgeSets->find(set); + if (it != badgeSets->end()) { + auto it2 = it->second.find(version); + if (it2 != it->second.end()) { + return it2->second; + } + } + return boost::none; +} + } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 7b4e8a638..8b2edeab4 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -2,15 +2,18 @@ #include +#include "common/Atomic.hpp" #include "common/Channel.hpp" #include "common/Common.hpp" -#include "common/MutexValue.hpp" +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" #include "singletons/Emotes.hpp" #include "util/ConcurrentMap.hpp" #include #include +#include namespace chatterino { @@ -18,9 +21,6 @@ class TwitchServer; class TwitchChannel final : public Channel, pajlada::Signals::SignalHolder { - QTimer *liveStatusTimer; - QTimer *chattersListTimer; - public: struct StreamStatus { bool live = false; @@ -46,84 +46,111 @@ public: QString broadcasterLang; }; - ~TwitchChannel() final; + void refreshChannelEmotes(); - void reloadChannelEmotes(); + // Channel methods + virtual bool isEmpty() const override; + virtual bool canSendMessage() const override; + virtual void sendMessage(const QString &message) override; - bool isEmpty() const override; - bool canSendMessage() const override; - void sendMessage(const QString &message) override; + // Auto completion + void addRecentChatter(const MessagePtr &message) final; + void addJoinedUser(const QString &user); + void addPartedUser(const QString &user); + // Twitch data + bool isLive() const; virtual bool isMod() const override; void setMod(bool value); virtual bool isBroadcaster() const override; - void addRecentChatter(const std::shared_ptr &message) final; - void addJoinedUser(const QString &user); - void addPartedUser(const QString &user); + QString roomId() const; + void setRoomId(const QString &id); + AccessGuard accessRoomModes() const; + void setRoomModes(const RoomModes &roomModes_); + AccessGuard accessStreamStatus() const; - const std::shared_ptr bttvChannelEmotes; - const std::shared_ptr ffzChannelEmotes; + boost::optional bttvEmote(const EmoteName &name) const; + boost::optional ffzEmote(const EmoteName &name) const; + std::shared_ptr bttvEmotes() const; + std::shared_ptr ffzEmotes() const; + const QString &subscriptionUrl(); + const QString &channelUrl(); + const QString &popoutPlayerUrl(); - const QString subscriptionURL; - const QString channelURL; - const QString popoutPlayerURL; + boost::optional getTwitchBadge(const QString &set, + const QString &version) const; - void setRoomID(const QString &_roomID); - pajlada::Signals::NoArgSignal roomIDchanged; - pajlada::Signals::NoArgSignal updateLiveInfo; - - pajlada::Signals::NoArgBoltSignal fetchMessages; + // Signals + pajlada::Signals::NoArgSignal roomIdChanged; + pajlada::Signals::NoArgSignal liveStatusChanged; pajlada::Signals::NoArgSignal userStateChanged; pajlada::Signals::NoArgSignal roomModesChanged; - QString roomID; - - RoomModes getRoomModes(); - void setRoomModes(const RoomModes &roomModes_); - - StreamStatus getStreamStatus() const; - +private: struct NameOptions { QString displayName; QString localizedName; }; - bool isLive() const; + struct CheerEmote { + // a Cheermote indicates one tier + QColor color; + int minBits; -private: - explicit TwitchChannel(const QString &channelName, Communi::IrcConnection *readConnection); + EmotePtr animatedEmote; + EmotePtr staticEmote; + }; + + struct CheerEmoteSet { + QRegularExpression regex; + std::vector cheerEmotes; + }; + + explicit TwitchChannel(const QString &channelName); + + // Methods + void refreshLiveStatus(); + Outcome parseLiveStatus(const rapidjson::Document &document); + void refreshPubsub(); + void refreshViewerList(); + Outcome parseViewerList(const QJsonObject &jsonRoot); + void loadRecentMessages(); void setLive(bool newLiveStatus); - void refreshLiveStatus(); - void startRefreshLiveStatusTimer(int intervalMS); - void fetchRecentMessages(); - mutable std::mutex streamStatusMutex_; - StreamStatus streamStatus_; + void loadBadges(); + void loadCheerEmotes(); - mutable std::mutex userStateMutex_; - UserState userState_; + // Twitch data + UniqueAccess streamStatus_; + UniqueAccess userState_; + UniqueAccess roomModes_; + + Atomic> bttvEmotes_; + Atomic> ffzEmotes_; + const QString subscriptionUrl_; + const QString channelUrl_; + const QString popoutPlayerUrl_; bool mod_ = false; - QByteArray messageSuffix_; - QString lastSentMessage_; - RoomModes roomModes_; - std::mutex roomModeMutex_; + UniqueAccess roomID_; - QObject object_; - std::mutex joinedUserMutex_; - QStringList joinedUsers_; + UniqueAccess joinedUsers_; bool joinedUsersMergeQueued_ = false; - std::mutex partedUserMutex_; - QStringList partedUsers_; + UniqueAccess partedUsers_; bool partedUsersMergeQueued_ = false; - Communi::IrcConnection *readConnection_ = nullptr; + // "subscribers": { "0": ... "3": ... "6": ... + UniqueAccess>> badgeSets_; + UniqueAccess> cheerEmoteSets_; - // Key = login name - std::map recentChatters_; - std::mutex recentChattersMutex_; + // -- + QByteArray messageSuffix_; + QString lastSentMessage_; + QObject lifetimeGuard_; + QTimer liveStatusTimer_; + QTimer chattersListTimer_; friend class TwitchServer; }; diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index d067f320e..08c28286f 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -1,233 +1,73 @@ #include "providers/twitch/TwitchEmotes.hpp" -#include "common/UrlFetch.hpp" +#include "common/NetworkRequest.hpp" #include "debug/Benchmark.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" -#define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" - namespace chatterino { -namespace { - -QString getEmoteLink(const QString &id, const QString &emoteScale) -{ - QString value = TWITCH_EMOTE_TEMPLATE; - - value.detach(); - - return value.replace("{id}", id).replace("{scale}", emoteScale); -} - -QString cleanUpCode(const QString &dirtyEmoteCode) -{ - QString cleanCode = dirtyEmoteCode; - // clang-format off - static QMap emoteNameReplacements{ - {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, - {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, - {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, - {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, - {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, - }; - // clang-format on - - auto it = emoteNameReplacements.find(dirtyEmoteCode); - if (it != emoteNameReplacements.end()) { - cleanCode = it.value(); - } - - cleanCode.replace("<", "<"); - cleanCode.replace(">", ">"); - - return cleanCode; -} - -} // namespace - TwitchEmotes::TwitchEmotes() { - { - EmoteSet emoteSet; - emoteSet.key = "19194"; - emoteSet.text = "Twitch Prime Emotes"; - this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); - } - - { - EmoteSet emoteSet; - emoteSet.key = "0"; - emoteSet.text = "Twitch Global Emotes"; - this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); - } } // id is used for lookup // emoteName is used for giving a name to the emote in case it doesn't exist -EmoteData TwitchEmotes::getEmoteById(const QString &id, const QString &emoteName) +EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, + const EmoteName &name_) { - QString _emoteName = emoteName; - _emoteName.replace("<", "<"); - _emoteName.replace(">", ">"); - - // clang-format off - static QMap emoteNameReplacements{ - {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, - {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, - {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, - {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, + static QMap replacements{ + {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, + {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, + {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, + {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, + {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, + {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, }; - // clang-format on - auto it = emoteNameReplacements.find(_emoteName); - if (it != emoteNameReplacements.end()) { - _emoteName = it.value(); + auto name = name_.string; + name.detach(); + + // replace < > + name.replace("<", "<"); + name.replace(">", ">"); + + // replace regexes + auto it = replacements.find(name); + if (it != replacements.end()) { + name = it.value(); } - return twitchEmoteFromCache_.getOrAdd(id, [&emoteName, &_emoteName, &id] { - EmoteData newEmoteData; - auto cleanCode = cleanUpCode(emoteName); - newEmoteData.image1x = - new Image(getEmoteLink(id, "1.0"), 1, emoteName, _emoteName + "
Twitch Emote"); - newEmoteData.image1x->setCopyString(cleanCode); + // search in cache or create new emote + auto cache = this->twitchEmotesCache_.access(); + auto shared = (*cache)[id].lock(); - newEmoteData.image2x = - new Image(getEmoteLink(id, "2.0"), .5, emoteName, _emoteName + "
Twitch Emote"); - newEmoteData.image2x->setCopyString(cleanCode); + if (!shared) { + (*cache)[id] = shared = std::make_shared( + Emote{EmoteName{name}, + ImageSet{ + Image::fromUrl(getEmoteLink(id, "1.0"), 1), + Image::fromUrl(getEmoteLink(id, "2.0"), 0.5), + Image::fromUrl(getEmoteLink(id, "3.0"), 0.25), + }, + Tooltip{name}, Url{}}); + } - newEmoteData.image3x = - new Image(getEmoteLink(id, "3.0"), .25, emoteName, _emoteName + "
Twitch Emote"); - - newEmoteData.image3x->setCopyString(cleanCode); - - return newEmoteData; - }); + return shared; } -void TwitchEmotes::refresh(const std::shared_ptr &user) +Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) { - const auto &roomID = user->getUserId(); - TwitchAccountEmoteData &emoteData = this->emotes[roomID]; - - if (emoteData.filled) { - Log("Emotes are already loaded for room id {}", roomID); - return; - } - - auto loadEmotes = [=, &emoteData](const rapidjson::Document &root) { - emoteData.emoteSets.clear(); - emoteData.emoteCodes.clear(); - - auto emoticonSets = root.FindMember("emoticon_sets"); - if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject()) { - Log("No emoticon_sets in load emotes response"); - return; - } - - for (const auto &emoteSetJSON : emoticonSets->value.GetObject()) { - auto emoteSet = std::make_shared(); - - emoteSet->key = emoteSetJSON.name.GetString(); - - this->loadSetData(emoteSet); - - for (const rapidjson::Value &emoteJSON : emoteSetJSON.value.GetArray()) { - if (!emoteJSON.IsObject()) { - Log("Emote value was invalid"); - return; - } - - QString id, code; - - uint64_t idNumber; - - if (!rj::getSafe(emoteJSON, "id", idNumber)) { - Log("No ID key found in Emote value"); - return; - } - - if (!rj::getSafe(emoteJSON, "code", code)) { - Log("No code key found in Emote value"); - return; - } - - id = QString::number(idNumber); - - auto cleanCode = cleanUpCode(code); - emoteSet->emotes.emplace_back(id, cleanCode); - emoteData.emoteCodes.push_back(cleanCode); - - EmoteData emote = this->getEmoteById(id, code); - emoteData.emotes.insert(code, emote); - } - - emoteData.emoteSets.emplace_back(emoteSet); - } - - emoteData.filled = true; - }; - - user->loadEmotes(loadEmotes); + return {QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", id.string) + .replace("{scale}", emoteScale)}; } -void TwitchEmotes::loadSetData(std::shared_ptr emoteSet) +AccessGuard> TwitchEmotes::accessAll() { - if (!emoteSet) { - Log("null emote set sent"); - return; - } - - auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); - if (staticSetIt != this->staticEmoteSets.end()) { - const auto &staticSet = staticSetIt->second; - emoteSet->channelName = staticSet.channelName; - emoteSet->text = staticSet.text; - return; - } - - NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" + emoteSet->key + - "/"); - req.setUseQuickLoadCache(true); - - req.onError([](int errorCode) -> bool { - Log("Error code {} while loading emote set data", errorCode); - return true; - }); - - req.onSuccess([emoteSet](auto result) -> bool { - auto root = result.parseRapidJson(); - if (!root.IsObject()) { - return false; - } - - std::string emoteSetID; - QString channelName; - QString type; - if (!rj::getSafe(root, "channel_name", channelName)) { - return false; - } - - if (!rj::getSafe(root, "type", type)) { - return false; - } - - Log("Loaded twitch emote set data for {}!", emoteSet->key); - - if (type == "sub") { - emoteSet->text = QString("Twitch Subscriber Emote (%1)").arg(channelName); - } else { - emoteSet->text = QString("Twitch Account Emote (%1)").arg(channelName); - } - - emoteSet->channelName = channelName; - - return true; - }); - - req.execute(); + return this->twitchEmotes_.access(); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index 295fca634..69cf65e5e 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -1,14 +1,17 @@ #pragma once -#include "common/Emotemap.hpp" +#include +#include + +#include "common/UniqueAccess.hpp" +#include "messages/Emote.hpp" #include "providers/twitch/EmoteValue.hpp" #include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "util/ConcurrentMap.hpp" -#include - -#include +#define TWITCH_EMOTE_TEMPLATE \ + "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" namespace chatterino { @@ -17,55 +20,14 @@ class TwitchEmotes public: TwitchEmotes(); - EmoteData getEmoteById(const QString &id, const QString &emoteName); - - /// Twitch emotes - void refresh(const std::shared_ptr &user); - - struct TwitchEmote { - TwitchEmote(const QString &_id, const QString &_code) - : id(_id) - , code(_code) - { - } - - // i.e. "403921" - QString id; - - // i.e. "forsenE" - QString code; - }; - - struct EmoteSet { - QString key; - QString channelName; - QString text; - std::vector emotes; - }; - - std::map staticEmoteSets; - - struct TwitchAccountEmoteData { - std::vector> emoteSets; - - std::vector emoteCodes; - - EmoteMap emotes; - - bool filled = false; - }; - - // Key is the user ID - std::map emotes; + EmotePtr getOrCreateEmote(const EmoteId &id, const EmoteName &name); + Url getEmoteLink(const EmoteId &id, const QString &emoteScale); + AccessGuard> accessAll(); private: - void loadSetData(std::shared_ptr emoteSet); - - // emote code - ConcurrentMap twitchEmotes_; - - // emote id - ConcurrentMap twitchEmoteFromCache_; + UniqueAccess> twitchEmotes_; + UniqueAccess>> + twitchEmotesCache_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchHelpers.cpp b/src/providers/twitch/TwitchHelpers.cpp index f236b10cb..9999e125c 100644 --- a/src/providers/twitch/TwitchHelpers.cpp +++ b/src/providers/twitch/TwitchHelpers.cpp @@ -6,7 +6,7 @@ namespace chatterino { bool trimChannelName(const QString &channelName, QString &outChannelName) { if (channelName.length() < 3) { - Log("channel name length below 3"); + log("channel name length below 3"); return false; } diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index a9337e3f6..3fafb7254 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace chatterino { @@ -30,8 +31,7 @@ TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel, , originalMessage_(_ircMessage->content()) , action_(_ircMessage->isAction()) { - auto app = getApp(); - this->usernameColor_ = app->themes->messages.textColors.system; + this->usernameColor_ = getApp()->themes->messages.textColors.system; } TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel, @@ -46,8 +46,7 @@ TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel, , originalMessage_(content) , action_(isAction) { - auto app = getApp(); - this->usernameColor_ = app->themes->messages.textColors.system; + this->usernameColor_ = getApp()->themes->messages.textColors.system; } bool TwitchMessageBuilder::isIgnored() const @@ -57,7 +56,7 @@ bool TwitchMessageBuilder::isIgnored() const // TODO(pajlada): Do we need to check if the phrase is valid first? for (const auto &phrase : app->ignores->phrases.getVector()) { if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) { - Log("Blocking message because it contains ignored phrase {}", phrase.getPattern()); + log("Blocking message because it contains ignored phrase {}", phrase.getPattern()); return true; } } @@ -67,7 +66,7 @@ bool TwitchMessageBuilder::isIgnored() const for (const auto &user : app->accounts->twitch.getCurrent()->getIgnores()) { if (sourceUserID == user.id) { - Log("Blocking message because it's from blocked user {}", user.name); + log("Blocking message because it's from blocked user {}", user.name); return true; } } @@ -83,18 +82,18 @@ MessagePtr TwitchMessageBuilder::build() // PARSING this->parseUsername(); - if (this->userName == this->channel->name) { + if (this->userName == this->channel->getName()) { this->senderIsBroadcaster = true; } //#ifdef XD // if (this->originalMessage.length() > 100) { - // this->message->flags |= Message::Collapsed; + // this->message->flags.has(MessageFlag::Collapsed); // this->emplace(getApp()->resources->badgeCollapsed, - // MessageElement::Collapsed); + // MessageElementFlag::Collapsed); // } //#endif - this->message_->flags |= Message::Collapsed; + this->message().flags.has(MessageFlag::Collapsed); // PARSING this->parseMessageID(); @@ -141,18 +140,17 @@ MessagePtr TwitchMessageBuilder::build() this->appendUsername(); // highlights - if (/*app->settings->enableHighlights &&*/ !isPastMsg) { - this->parseHighlights(); - } + this->parseHighlights(isPastMsg); - QString bits; + // QString bits; auto iterator = this->tags.find("bits"); if (iterator != this->tags.end()) { - bits = iterator.value().toString(); + this->hasBits_ = true; + // bits = iterator.value().toString(); } // twitch emotes - std::vector> twitchEmotes; + std::vector> twitchEmotes; iterator = this->tags.find("emotes"); if (iterator != this->tags.end()) { @@ -170,7 +168,7 @@ MessagePtr TwitchMessageBuilder::build() return ((std::get<0>(item) >= pos) && std::get<0>(item) < (pos + len)); }); - std::vector> v(it, twitchEmotes.end()); + std::vector> v(it, twitchEmotes.end()); twitchEmotes.erase(it, twitchEmotes.end()); return v; }; @@ -208,9 +206,9 @@ MessagePtr TwitchMessageBuilder::build() for (auto &tup : vret) { int index = 0; const auto &emote = std::get<2>(tup); - while ((index = mid.indexOf(emote, index)) != -1) { + while ((index = mid.indexOf(emote.string, index)) != -1) { std::get<0>(tup) = from + index; - index += emote.size(); + index += emote.string.size(); twitchEmotes.push_back(tup); } } @@ -238,9 +236,9 @@ MessagePtr TwitchMessageBuilder::build() for (auto &tup : vret) { int index = 0; const auto &emote = std::get<2>(tup); - while ((index = replace.indexOf(emote, index)) != -1) { + while ((index = replace.indexOf(emote.string, index)) != -1) { std::get<0>(tup) = from + index; - index += emote.size(); + index += emote.string.size(); twitchEmotes.push_back(tup); } } @@ -257,108 +255,148 @@ MessagePtr TwitchMessageBuilder::build() auto currentTwitchEmote = twitchEmotes.begin(); // words - QStringList splits = this->originalMessage_.split(' '); - long int i = 0; + this->addWords(splits, twitchEmotes); - for (const auto &split : splits) { - MessageColor textColor = - this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text); + this->message().searchText = this->userName + ": " + this->originalMessage_; - // twitch emote + return this->release(); +} + +void TwitchMessageBuilder::addWords( + const QStringList &words, const std::vector> &twitchEmotes) +{ + auto i = int(); + auto currentTwitchEmote = twitchEmotes.begin(); + + for (const auto &word : words) { + // check if it's a twitch emote twitch emote if (currentTwitchEmote != twitchEmotes.end() && std::get<0>(*currentTwitchEmote) == i) { auto emoteImage = std::get<1>(*currentTwitchEmote); - this->emplace(emoteImage, MessageElement::TwitchEmote); + this->emplace(emoteImage, MessageElementFlag::TwitchEmote); - i += split.length() + 1; - currentTwitchEmote = std::next(currentTwitchEmote); + i += word.length() + 1; + currentTwitchEmote++; continue; } // split words - std::vector> parsed; - - // Parse emojis and take all non-emojis and put them in parsed as full text-words - app->emotes->emojis.parse(parsed, split); - - for (const auto &tuple : parsed) { - const EmoteData &emoteData = std::get<0>(tuple); - - if (!emoteData.isValid()) { // is text - QString string = std::get<1>(tuple); - - if (!bits.isEmpty() && this->tryParseCheermote(string)) { - // This string was parsed as a cheermote - continue; - } - - // TODO: Implement ignored emotes - // Format of ignored emotes: - // Emote name: "forsenPuke" - if string in ignoredEmotes - // Will match emote regardless of source (i.e. bttv, ffz) - // Emote source + name: "bttv:nyanPls" - if (this->tryAppendEmote(string)) { - // Successfully appended an emote - continue; - } - - // Actually just text - QString linkString = this->matchLink(string); - auto fontStyle = FontStyle::ChatMedium; - - if (string.startsWith('@') && app->settings->usernameBold) { - fontStyle = FontStyle::ChatMediumBold; - } - - Link link; - - if (linkString.isEmpty()) { - link = Link(); - } else { - if (app->settings->lowercaseLink) { - QRegularExpression httpRegex("\\bhttps?://", - QRegularExpression::CaseInsensitiveOption); - QRegularExpression ftpRegex("\\bftps?://", - QRegularExpression::CaseInsensitiveOption); - QRegularExpression getDomain("\\/\\/([^\\/]*)"); - QString tempString = string; - - if (!string.contains(httpRegex)) { - if (!string.contains(ftpRegex)) { - tempString.insert(0, "http://"); - } - } - QString domain = getDomain.match(tempString).captured(1); - string.replace(domain, domain.toLower()); - } - link = Link(Link::Url, linkString); - textColor = MessageColor(MessageColor::Link); - } - - this->emplace(string, MessageElement::Text, textColor, - fontStyle) // - ->setLink(link); - } else { // is emoji - this->emplace(emoteData, EmoteElement::EmojiAll); - } + for (auto &variant : getApp()->emotes->emojis.parse(word)) { + boost::apply_visitor(/*overloaded{[&](EmotePtr arg) { + this->addTextOrEmoji(arg); }, + [&](const QString &arg) { + this->addTextOrEmoji(arg); }}*/ + [&](auto &&arg) { this->addTextOrEmoji(arg); }, variant); } - for (int j = 0; j < split.size(); j++) { + for (int j = 0; j < word.size(); j++) { i++; - if (split.at(j).isHighSurrogate()) { + if (word.at(j).isHighSurrogate()) { j++; } } i++; } +} - this->message_->searchText = this->userName + ": " + this->originalMessage_; +void TwitchMessageBuilder::addTextOrEmoji(EmotePtr emote) +{ + this->emplace(emote, MessageElementFlag::EmojiAll); +} - return this->getMessage(); +void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) +{ + auto string = QString(string_); + + if (this->hasBits_ && this->tryParseCheermote(string)) { + // This string was parsed as a cheermote + return; + } + + // TODO: Implement ignored emotes + // Format of ignored emotes: + // Emote name: "forsenPuke" - if string in ignoredEmotes + // Will match emote regardless of source (i.e. bttv, ffz) + // Emote source + name: "bttv:nyanPls" + if (this->tryAppendEmote({string})) { + // Successfully appended an emote + return; + } + + // Actually just text + auto linkString = this->matchLink(string); + auto link = Link(); + auto textColor = + this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text); + + if (linkString.isEmpty()) { + if (string.startsWith('@')) { + this->emplace(string, MessageElementFlag::BoldUsername, textColor, + FontStyle::ChatMediumBold); + this->emplace(string, MessageElementFlag::NonBoldUsername, textColor); + } else { + this->emplace(string, MessageElementFlag::Text, textColor); + } + } else { + static QRegularExpression domainRegex( + R"(^(?:(?:ftp|http)s?:\/\/)?([^\/:]+)(?:\/.*)?$)", + QRegularExpression::CaseInsensitiveOption); + + QString lowercaseLinkString; + auto match = domainRegex.match(string); + if (match.isValid()) { + lowercaseLinkString = string.mid(0, match.capturedStart(1)) + + match.captured(1).toLower() + string.mid(match.capturedEnd(1)); + } else { + lowercaseLinkString = string; + } + link = Link(Link::Url, linkString); + + textColor = MessageColor(MessageColor::Link); + this->emplace(lowercaseLinkString, MessageElementFlag::LowercaseLink, + textColor) + ->setLink(link); + this->emplace(string, MessageElementFlag::OriginalLink, textColor) + ->setLink(link); + } + + // if (!linkString.isEmpty()) { + // if (getApp()->settings->lowercaseLink) { + // QRegularExpression httpRegex("\\bhttps?://", + // QRegularExpression::CaseInsensitiveOption); QRegularExpression + // ftpRegex("\\bftps?://", + // QRegularExpression::CaseInsensitiveOption); QRegularExpression + // getDomain("\\/\\/([^\\/]*)"); QString tempString = string; + + // if (!string.contains(httpRegex)) { + // if (!string.contains(ftpRegex)) { + // tempString.insert(0, "http://"); + // } + // } + // QString domain = getDomain.match(tempString).captured(1); + // string.replace(domain, domain.toLower()); + // } + // link = Link(Link::Url, linkString); + // textColor = MessageColor(MessageColor::Link); + //} + // if (string.startsWith('@')) { + // this->emplace(string, MessageElementFlag::BoldUsername, + // textColor, + // FontStyle::ChatMediumBold) // + // ->setLink(link); + // this->emplace(string, + // MessageElementFlag::NonBoldUsername, + // textColor) // + // ->setLink(link); + //} else { + // this->emplace(string, MessageElementFlag::Text, + // textColor) // + // ->setLink(link); + //} } void TwitchMessageBuilder::parseMessageID() @@ -381,18 +419,19 @@ void TwitchMessageBuilder::parseRoomID() if (iterator != std::end(this->tags)) { this->roomID_ = iterator.value().toString(); - if (this->twitchChannel->roomID.isEmpty()) { - this->twitchChannel->roomID = this->roomID_; + if (this->twitchChannel->roomId().isEmpty()) { + this->twitchChannel->setRoomId(this->roomID_); } } } void TwitchMessageBuilder::appendChannelName() { - QString channelName("#" + this->channel->name); - Link link(Link::Url, this->channel->name + "\n" + this->messageID); + QString channelName("#" + this->channel->getName()); + Link link(Link::Url, this->channel->getName() + "\n" + this->messageID); - this->emplace(channelName, MessageElement::ChannelName, MessageColor::System) // + this->emplace(channelName, MessageElementFlag::ChannelName, + MessageColor::System) // ->setLink(link); } @@ -413,10 +452,11 @@ void TwitchMessageBuilder::parseUsername() // display name // auto displayNameVariant = this->tags.value("display-name"); // if (displayNameVariant.isValid()) { - // this->userName = displayNameVariant.toString() + " (" + this->userName + ")"; + // this->userName = displayNameVariant.toString() + " (" + + // this->userName + ")"; // } - this->message_->loginName = this->userName; + this->message().loginName = this->userName; } void TwitchMessageBuilder::appendUsername() @@ -424,7 +464,7 @@ void TwitchMessageBuilder::appendUsername() auto app = getApp(); QString username = this->userName; - this->message_->loginName = username; + this->message().loginName = username; QString localizedName; auto iterator = this->tags.find("display-name"); @@ -434,12 +474,12 @@ void TwitchMessageBuilder::appendUsername() if (QString::compare(displayName, this->userName, Qt::CaseInsensitive) == 0) { username = displayName; - this->message_->displayName = displayName; + this->message().displayName = displayName; } else { localizedName = displayName; - this->message_->displayName = username; - this->message_->localizedName = displayName; + this->message().displayName = username; + this->message().localizedName = displayName; } } @@ -476,39 +516,40 @@ void TwitchMessageBuilder::appendUsername() if (this->args.isSentWhisper) { // TODO(pajlada): Re-implement - // userDisplayString += IrcManager::getInstance().getUser().getUserName(); + // userDisplayString += + // IrcManager::getInstance().getUser().getUserName(); } else if (this->args.isReceivedWhisper) { // Sender username - this->emplace(usernameText, MessageElement::Text, this->usernameColor_, + this->emplace(usernameText, MessageElementFlag::Text, this->usernameColor_, FontStyle::ChatMediumBold) ->setLink({Link::UserInfo, this->userName}); auto currentUser = app->accounts->twitch.getCurrent(); // Separator - this->emplace("->", MessageElement::Text, + this->emplace("->", MessageElementFlag::Text, app->themes->messages.textColors.system, FontStyle::ChatMedium); - QColor selfColor = currentUser->color; + QColor selfColor = currentUser->color(); if (!selfColor.isValid()) { selfColor = app->themes->messages.textColors.system; } // Your own username - this->emplace(currentUser->getUserName() + ":", MessageElement::Text, + this->emplace(currentUser->getUserName() + ":", MessageElementFlag::Text, selfColor, FontStyle::ChatMediumBold); } else { if (!this->action_) { usernameText += ":"; } - this->emplace(usernameText, MessageElement::Text, this->usernameColor_, + this->emplace(usernameText, MessageElementFlag::Text, this->usernameColor_, FontStyle::ChatMediumBold) ->setLink({Link::UserInfo, this->userName}); } } -void TwitchMessageBuilder::parseHighlights() +void TwitchMessageBuilder::parseHighlights(bool isPastMsg) { static auto player = new QMediaPlayer; static QUrl currentPlayerUrl; @@ -520,7 +561,7 @@ void TwitchMessageBuilder::parseHighlights() QString currentUsername = currentUser->getUserName(); if (this->ircMessage->nick() == currentUsername) { - currentUser->color = this->usernameColor_; + currentUser->setColor(this->usernameColor_); // Do nothing. Highlights cannot be triggered by yourself return; } @@ -528,7 +569,7 @@ void TwitchMessageBuilder::parseHighlights() // update the media player url if necessary QUrl highlightSoundUrl; if (app->settings->customHighlightSound) { - highlightSoundUrl = QUrl(app->settings->pathHighlightSound.getValue()); + highlightSoundUrl = QUrl::fromLocalFile(app->settings->pathHighlightSound.getValue()); } else { highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav"); } @@ -559,7 +600,7 @@ void TwitchMessageBuilder::parseHighlights() if (!app->highlights->blacklistContains(this->ircMessage->nick())) { for (const HighlightPhrase &highlight : activeHighlights) { if (highlight.isMatch(this->originalMessage_)) { - Log("Highlight because {} matches {}", this->originalMessage_, + log("Highlight because {} matches {}", this->originalMessage_, highlight.getPattern()); doHighlight = true; @@ -572,15 +613,16 @@ void TwitchMessageBuilder::parseHighlights() } if (playSound && doAlert) { - // Break if no further action can be taken from other highlights - // This might change if highlights can have custom colors/sounds/actions + // Break if no further action can be taken from other + // highlights This might change if highlights can have + // custom colors/sounds/actions break; } } } for (const HighlightPhrase &userHighlight : userHighlights) { if (userHighlight.isMatch(this->ircMessage->nick())) { - Log("Highlight because user {} sent a message", this->ircMessage->nick()); + log("Highlight because user {} sent a message", this->ircMessage->nick()); doHighlight = true; if (userHighlight.getAlert()) { @@ -592,106 +634,97 @@ void TwitchMessageBuilder::parseHighlights() } if (playSound && doAlert) { - // Break if no further action can be taken from other usernames - // Mostly used for regex stuff + // Break if no further action can be taken from other + // usernames Mostly used for regex stuff break; } } } - this->setHighlight(doHighlight); + this->message().flags.set(MessageFlag::Highlighted, doHighlight); - if (playSound && (!hasFocus || app->settings->highlightAlwaysPlaySound)) { - player->play(); - } + if (!isPastMsg) { + if (playSound && (!hasFocus || app->settings->highlightAlwaysPlaySound)) { + player->play(); + } - if (doAlert) { - QApplication::alert(getApp()->windows->getMainWindow().window(), 2500); - } - - if (doHighlight) { - this->message_->flags |= Message::Highlighted; + if (doAlert) { + QApplication::alert(getApp()->windows->getMainWindow().window(), 2500); + } } } } -void TwitchMessageBuilder::appendTwitchEmote( - const Communi::IrcMessage *ircMessage, const QString &emote, - std::vector> &vec) +void TwitchMessageBuilder::appendTwitchEmote(const Communi::IrcMessage *ircMessage, + const QString &emote, + std::vector> &vec) { auto app = getApp(); if (!emote.contains(':')) { return; } - QStringList parameters = emote.split(':'); + auto parameters = emote.split(':'); if (parameters.length() < 2) { return; } - const auto &id = parameters.at(0); + auto id = EmoteId{parameters.at(0)}; - QStringList occurences = parameters.at(1).split(','); + auto occurences = parameters.at(1).split(','); for (QString occurence : occurences) { - QStringList coords = occurence.split('-'); + auto coords = occurence.split('-'); if (coords.length() < 2) { return; } - int start = coords.at(0).toInt(); - int end = coords.at(1).toInt(); + auto start = coords.at(0).toInt(); + auto end = coords.at(1).toInt(); if (start >= end || start < 0 || end > this->originalMessage_.length()) { return; } - QString name = this->originalMessage_.mid(start, end - start + 1); + auto name = EmoteName{this->originalMessage_.mid(start, end - start + 1)}; - vec.push_back(std::tuple( - start, app->emotes->twitch.getEmoteById(id, name), name)); + vec.push_back(std::tuple{ + start, app->emotes->twitch.getOrCreateEmote(id, name), name}); } } -bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString) +Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) { - auto app = getApp(); - EmoteData emoteData; + auto flags = MessageElementFlags(); + auto emote = boost::optional{}; - auto appendEmote = [&](MessageElement::Flags flags) { - this->emplace(emoteData, flags); - return true; - }; - - if (app->emotes->bttv.globalEmotes.tryGet(emoteString, emoteData)) { - // BTTV Global Emote - return appendEmote(MessageElement::BttvEmote); - } else if (this->twitchChannel != nullptr && - this->twitchChannel->bttvChannelEmotes->tryGet(emoteString, emoteData)) { - // BTTV Channel Emote - return appendEmote(MessageElement::BttvEmote); - } else if (app->emotes->ffz.globalEmotes.tryGet(emoteString, emoteData)) { - // FFZ Global Emote - return appendEmote(MessageElement::FfzEmote); - } else if (this->twitchChannel != nullptr && - this->twitchChannel->ffzChannelEmotes->tryGet(emoteString, emoteData)) { - // FFZ Channel Emote - return appendEmote(MessageElement::FfzEmote); + if ((emote = getApp()->emotes->bttv.global(name))) { + flags = MessageElementFlag::BttvEmote; + } else if (twitchChannel && (emote = this->twitchChannel->bttvEmote(name))) { + flags = MessageElementFlag::BttvEmote; + } else if ((emote = getApp()->emotes->ffz.global(name))) { + flags = MessageElementFlag::FfzEmote; + } else if (twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) { + flags = MessageElementFlag::FfzEmote; } - return false; + if (emote) { + this->emplace(emote.get(), flags); + return Success; + } + + return Failure; } // fourtf: this is ugly -// maybe put the individual badges into a map instead of this mess +// maybe put the individual badges into a map instead of this +// mess void TwitchMessageBuilder::appendTwitchBadges() { auto app = getApp(); - const auto &channelResources = app->resources->channels[this->roomID_]; - auto iterator = this->tags.find("badges"); if (iterator == this->tags.end()) { @@ -707,209 +740,229 @@ void TwitchMessageBuilder::appendTwitchBadges() } if (badge.startsWith("bits/")) { - if (!app->resources->dynamicBadgesLoaded) { - // Do nothing - continue; - } + // if (!app->resources->dynamicBadgesLoaded) { + // // Do nothing + // continue; + // } - QString cheerAmountQS = badge.mid(5); - std::string versionKey = cheerAmountQS.toStdString(); - QString tooltip = QString("Twitch cheer ") + cheerAmountQS; + QString cheerAmount = badge.mid(5); + QString tooltip = QString("Twitch cheer ") + cheerAmount; // Try to fetch channel-specific bit badge try { - const auto &badge = channelResources.badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) - ->setTooltip(tooltip); - continue; + if (twitchChannel) + if (const auto &badge = + this->twitchChannel->getTwitchBadge("bits", cheerAmount)) { + this->emplace(badge.get(), MessageElementFlag::BadgeVanity) + ->setTooltip(tooltip); + continue; + } } catch (const std::out_of_range &) { // Channel does not contain a special bit badge for this version } // Use default bit badge - try { - const auto &badge = app->resources->badgeSets.at("bits").versions.at(versionKey); - this->emplace(badge.badgeImage1x, MessageElement::BadgeVanity) - ->setTooltip(tooltip); - } catch (const std::out_of_range &) { - Log("No default bit badge for version {} found", versionKey); - continue; - } + // try { + // const auto &badge = + // app->resources->badgeSets.at("bits").versions.at(cheerAmount); + // this->emplace(badge.badgeImage1x, + // MessageElementFlag::BadgeVanity) + // ->setTooltip(tooltip); + //} catch (const std::out_of_range &) { + // Log("No default bit badge for version {} found", cheerAmount); + // continue; + //} } else if (badge == "staff/1") { - this->emplace(app->resources->badgeStaff, - MessageElement::BadgeGlobalAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.staff), + MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Staff"); } else if (badge == "admin/1") { - this->emplace(app->resources->badgeAdmin, - MessageElement::BadgeGlobalAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.admin), + MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Admin"); } else if (badge == "global_mod/1") { - this->emplace(app->resources->badgeGlobalModerator, - MessageElement::BadgeGlobalAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.globalmod), + MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Global Moderator"); } else if (badge == "moderator/1") { // TODO: Implement custom FFZ moderator badge - this->emplace(app->resources->badgeModerator, - MessageElement::BadgeChannelAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.moderator), + MessageElementFlag::BadgeChannelAuthority) ->setTooltip("Twitch Channel Moderator"); } else if (badge == "turbo/1") { - this->emplace(app->resources->badgeTurbo, - MessageElement::BadgeGlobalAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.turbo), + MessageElementFlag::BadgeGlobalAuthority) ->setTooltip("Twitch Turbo Subscriber"); } else if (badge == "broadcaster/1") { - this->emplace(app->resources->badgeBroadcaster, - MessageElement::BadgeChannelAuthority) + this->emplace(Image::fromPixmap(app->resources->twitch.broadcaster), + MessageElementFlag::BadgeChannelAuthority) ->setTooltip("Twitch Broadcaster"); } else if (badge == "premium/1") { - this->emplace(app->resources->badgePremium, MessageElement::BadgeVanity) + this->emplace(Image::fromPixmap(app->resources->twitch.prime), + MessageElementFlag::BadgeVanity) ->setTooltip("Twitch Prime Subscriber"); } else if (badge.startsWith("partner/")) { int index = badge.midRef(8).toInt(); switch (index) { case 1: { - this->emplace(app->resources->badgeVerified, - MessageElement::BadgeVanity) + this->emplace( + Image::fromPixmap(app->resources->twitch.verified, 0.25), + MessageElementFlag::BadgeVanity) ->setTooltip("Twitch Verified"); } break; default: { - printf("[TwitchMessageBuilder] Unhandled partner badge index: %d\n", index); + printf("[TwitchMessageBuilder] Unhandled partner badge " + "index: %d\n", + index); } break; } } else if (badge.startsWith("subscriber/")) { - if (channelResources.loaded == false) { - // qDebug() << "Channel resources are not loaded, can't add the subscriber badge"; - continue; - } + // if (channelResources.loaded == false) { + // // qDebug() << "Channel resources are not loaded, + // can't add the subscriber + // // badge"; + // continue; + // } - auto badgeSetIt = channelResources.badgeSets.find("subscriber"); - if (badgeSetIt == channelResources.badgeSets.end()) { - // Fall back to default badge - this->emplace(app->resources->badgeSubscriber, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch Subscriber"); - continue; - } + // auto badgeSetIt = channelResources.badgeSets.find("subscriber"); + // if (badgeSetIt == channelResources.badgeSets.end()) { + // // Fall back to default badge + // this->emplace(app->resources->badgeSubscriber, + // MessageElementFlag::BadgeSubscription) + // ->setTooltip("Twitch Subscriber"); + // continue; + //} - const auto &badgeSet = badgeSetIt->second; + // const auto &badgeSet = badgeSetIt->second; - std::string versionKey = badge.mid(11).toStdString(); + // std::string versionKey = badge.mid(11).toStdString(); - auto badgeVersionIt = badgeSet.versions.find(versionKey); + // auto badgeVersionIt = badgeSet.versions.find(versionKey); - if (badgeVersionIt == badgeSet.versions.end()) { - // Fall back to default badge - this->emplace(app->resources->badgeSubscriber, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch Subscriber"); - continue; - } + // if (badgeVersionIt == badgeSet.versions.end()) { + // // Fall back to default badge + // this->emplace(app->resources->badgeSubscriber, + // MessageElementFlag::BadgeSubscription) + // ->setTooltip("Twitch Subscriber"); + // continue; + //} - auto &badgeVersion = badgeVersionIt->second; + // auto &badgeVersion = badgeVersionIt->second; - this->emplace(badgeVersion.badgeImage1x, - MessageElement::BadgeSubscription) - ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); + // this->emplace(badgeVersion.badgeImage1x, + // MessageElementFlag::BadgeSubscription) + // ->setTooltip("Twitch " + + // QString::fromStdString(badgeVersion.title)); } else { - if (!app->resources->dynamicBadgesLoaded) { - // Do nothing - continue; - } + // if (!app->resources->dynamicBadgesLoaded) { + // // Do nothing + // continue; + //} - QStringList parts = badge.split('/'); + // QStringList parts = badge.split('/'); - if (parts.length() != 2) { - qDebug() << "Bad number of parts: " << parts.length() << " in " << parts; - continue; - } + // if (parts.length() != 2) { + // qDebug() << "Bad number of parts: " << parts.length() << " in + // " << parts; continue; + //} - MessageElement::Flags badgeType = MessageElement::Flags::BadgeVanity; + // MessageElementFlags badgeType = + // MessageElementFlag::BadgeVanity; - std::string badgeSetKey = parts[0].toStdString(); - std::string versionKey = parts[1].toStdString(); + // std::string badgeSetKey = parts[0].toStdString(); + // std::string versionKey = parts[1].toStdString(); - try { - auto &badgeSet = app->resources->badgeSets.at(badgeSetKey); + // try { + // auto &badgeSet = app->resources->badgeSets.at(badgeSetKey); - try { - auto &badgeVersion = badgeSet.versions.at(versionKey); + // try { + // auto &badgeVersion = badgeSet.versions.at(versionKey); - this->emplace(badgeVersion.badgeImage1x, badgeType) - ->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title)); - } catch (const std::exception &e) { - qDebug() << "Exception caught:" << e.what() - << "when trying to fetch badge version " << versionKey.c_str(); - } - } catch (const std::exception &e) { - qDebug() << "No badge set with key" << badgeSetKey.c_str() - << ". Exception: " << e.what(); - } + // this->emplace(badgeVersion.badgeImage1x, + // badgeType) + // ->setTooltip("Twitch " + + // QString::fromStdString(badgeVersion.title)); + // } catch (const std::exception &e) { + // qDebug() << "Exception caught:" << e.what() + // << "when trying to fetch badge version " << + // versionKey.c_str(); + // } + //} catch (const std::exception &e) { + // qDebug() << "No badge set with key" << badgeSetKey.c_str() + // << ". Exception: " << e.what(); + //} } } } void TwitchMessageBuilder::appendChatterinoBadges() { - auto app = getApp(); + // auto app = getApp(); - auto &badges = app->resources->chatterinoBadges; - auto it = badges.find(this->userName.toStdString()); + // auto &badges = app->resources->chatterinoBadges; + // auto it = badges.find(this->userName.toStdString()); - if (it == badges.end()) { - return; - } + // if (it == badges.end()) { + // return; + // } - const auto badge = it->second; + // const auto badge = it->second; - this->emplace(badge->image, MessageElement::BadgeChatterino) - ->setTooltip(QString::fromStdString(badge->tooltip)); + // this->emplace(badge->image, + // MessageElementFlag::BadgeChatterino) + // ->setTooltip(QString::fromStdString(badge->tooltip)); } -bool TwitchMessageBuilder::tryParseCheermote(const QString &string) +Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) { - auto app = getApp(); - // Try to parse custom cheermotes - const auto &channelResources = app->resources->channels[this->roomID_]; - if (channelResources.loaded) { - for (const auto &cheermoteSet : channelResources.cheermoteSets) { - auto match = cheermoteSet.regex.match(string); - if (!match.hasMatch()) { - continue; - } - QString amount = match.captured(1); - bool ok = false; - int numBits = amount.toInt(&ok); - if (!ok) { - Log("Error parsing bit amount in tryParseCheermote"); - return false; - } + // auto app = getApp(); + //// Try to parse custom cheermotes + // const auto &channelResources = app->resources->channels[this->roomID_]; + // if (channelResources.loaded) { + // for (const auto &cheermoteSet : channelResources.cheermoteSets) { + // auto match = cheermoteSet.regex.match(string); + // if (!match.hasMatch()) { + // continue; + // } + // QString amount = match.captured(1); + // bool ok = false; + // int numBits = amount.toInt(&ok); + // if (!ok) { + // Log("Error parsing bit amount in tryParseCheermote"); + // return Failure; + // } - auto savedIt = cheermoteSet.cheermotes.end(); + // auto savedIt = cheermoteSet.cheermotes.end(); - // Fetch cheermote that matches our numBits - for (auto it = cheermoteSet.cheermotes.begin(); it != cheermoteSet.cheermotes.end(); - ++it) { - if (numBits >= it->minBits) { - savedIt = it; - } else { - break; - } - } + // // Fetch cheermote that matches our numBits + // for (auto it = cheermoteSet.cheermotes.begin(); it != + // cheermoteSet.cheermotes.end(); + // ++it) { + // if (numBits >= it->minBits) { + // savedIt = it; + // } else { + // break; + // } + // } - if (savedIt == cheermoteSet.cheermotes.end()) { - Log("Error getting a cheermote from a cheermote set for the bit amount {}", - numBits); - return false; - } + // if (savedIt == cheermoteSet.cheermotes.end()) { + // Log("Error getting a cheermote from a cheermote set for the + // bit amount {}", + // numBits); + // return Failure; + // } - const auto &cheermote = *savedIt; + // const auto &cheermote = *savedIt; - this->emplace(cheermote.emoteDataAnimated, EmoteElement::BitsAnimated); - this->emplace(amount, EmoteElement::Text, cheermote.color); + // this->emplace(cheermote.animatedEmote, + // MessageElementFlag::BitsAnimated); + // this->emplace(amount, MessageElementFlag::Text, + // cheermote.color); - return true; - } - } + // return Success; + // } + //} - return false; + return Failure; } } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 246f3d50f..3195749c4 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -1,7 +1,6 @@ #pragma once #include "messages/MessageBuilder.hpp" -#include "messages/MessageParseArgs.hpp" #include "singletons/Emotes.hpp" #include @@ -48,17 +47,23 @@ private: void appendChannelName(); void parseUsername(); void appendUsername(); - void parseHighlights(); + void parseHighlights(bool isPastMsg); void appendTwitchEmote(const Communi::IrcMessage *ircMessage, const QString &emote, - std::vector > &vec); - bool tryAppendEmote(QString &emoteString); + std::vector> &vec); + Outcome tryAppendEmote(const EmoteName &name); + + void addWords(const QStringList &words, + const std::vector> &twitchEmotes); + void addTextOrEmoji(EmotePtr emote); + void addTextOrEmoji(const QString &value); void appendTwitchBadges(); void appendChatterinoBadges(); - bool tryParseCheermote(const QString &string); + Outcome tryParseCheermote(const QString &string); QString roomID_; + bool hasBits_ = false; QColor usernameColor_; QString originalMessage_; diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp new file mode 100644 index 000000000..47d6427f8 --- /dev/null +++ b/src/providers/twitch/TwitchParseCheerEmotes.cpp @@ -0,0 +1,274 @@ +#include "TwitchParseCheerEmotes.hpp" + +#include +#include +#include + +namespace chatterino { + +namespace { + +template +inline bool ReadValue(const rapidjson::Value &object, const char *key, + Type &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.Is()) { + return false; + } + + out = value.Get(); + + return true; +} + +template <> +inline bool ReadValue(const rapidjson::Value &object, const char *key, + QString &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.IsString()) { + return false; + } + + out = value.GetString(); + + return true; +} + +template <> +inline bool ReadValue>(const rapidjson::Value &object, + const char *key, + std::vector &out) +{ + if (!object.HasMember(key)) { + return false; + } + + const auto &value = object[key]; + + if (!value.IsArray()) { + return false; + } + + for (const rapidjson::Value &innerValue : value.GetArray()) { + if (!innerValue.IsString()) { + return false; + } + + out.emplace_back(innerValue.GetString()); + } + + return true; +} + +// Parse a single cheermote set (or "action") from the twitch api +inline bool ParseSingleCheermoteSet(JSONCheermoteSet &set, + const rapidjson::Value &action) +{ + if (!action.IsObject()) { + return false; + } + + if (!ReadValue(action, "prefix", set.prefix)) { + return false; + } + + if (!ReadValue(action, "scales", set.scales)) { + return false; + } + + if (!ReadValue(action, "backgrounds", set.backgrounds)) { + return false; + } + + if (!ReadValue(action, "states", set.states)) { + return false; + } + + if (!ReadValue(action, "type", set.type)) { + return false; + } + + if (!ReadValue(action, "updated_at", set.updatedAt)) { + return false; + } + + if (!ReadValue(action, "priority", set.priority)) { + return false; + } + + // Tiers + if (!action.HasMember("tiers")) { + return false; + } + + const auto &tiersValue = action["tiers"]; + + if (!tiersValue.IsArray()) { + return false; + } + + for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { + JSONCheermoteSet::CheermoteTier tier; + + if (!tierValue.IsObject()) { + return false; + } + + if (!ReadValue(tierValue, "min_bits", tier.minBits)) { + return false; + } + + if (!ReadValue(tierValue, "id", tier.id)) { + return false; + } + + if (!ReadValue(tierValue, "color", tier.color)) { + return false; + } + + // Images + if (!tierValue.HasMember("images")) { + return false; + } + + const auto &imagesValue = tierValue["images"]; + + if (!imagesValue.IsObject()) { + return false; + } + + // Read images object + for (const auto &imageBackgroundValue : imagesValue.GetObject()) { + QString background = imageBackgroundValue.name.GetString(); + bool backgroundExists = false; + for (const auto &bg : set.backgrounds) { + if (background == bg) { + backgroundExists = true; + break; + } + } + + if (!backgroundExists) { + continue; + } + + const rapidjson::Value &imageBackgroundStates = + imageBackgroundValue.value; + if (!imageBackgroundStates.IsObject()) { + continue; + } + + // Read each key which represents a background + for (const auto &imageBackgroundState : + imageBackgroundStates.GetObject()) { + QString state = imageBackgroundState.name.GetString(); + bool stateExists = false; + for (const auto &_state : set.states) { + if (state == _state) { + stateExists = true; + break; + } + } + + if (!stateExists) { + continue; + } + + const rapidjson::Value &imageScalesValue = + imageBackgroundState.value; + if (!imageScalesValue.IsObject()) { + continue; + } + + // Read each key which represents a scale + for (const auto &imageScaleValue : + imageScalesValue.GetObject()) { + QString scale = imageScaleValue.name.GetString(); + bool scaleExists = false; + for (const auto &_scale : set.scales) { + if (scale == _scale) { + scaleExists = true; + break; + } + } + + if (!scaleExists) { + continue; + } + + const rapidjson::Value &imageScaleURLValue = + imageScaleValue.value; + if (!imageScaleURLValue.IsString()) { + continue; + } + + QString url = imageScaleURLValue.GetString(); + + bool ok = false; + qreal scaleNumber = scale.toFloat(&ok); + if (!ok) { + continue; + } + + qreal chatterinoScale = 1 / scaleNumber; + + auto image = Image::fromUrl({url}, chatterinoScale); + + // TODO(pajlada): Fill in name and tooltip + tier.images[background][state][scale] = image; + } + } + } + + set.tiers.emplace_back(tier); + } + + return true; +} +} // namespace + +// Look through the results of +// https://api.twitch.tv/kraken/bits/actions?channel_id=11148817 for cheermote +// sets or "Actions" as they are called in the API +std::vector ParseCheermoteSets(const rapidjson::Document &d) +{ + std::vector sets; + + if (!d.IsObject()) { + return sets; + } + + if (!d.HasMember("actions")) { + return sets; + } + + const auto &actionsValue = d["actions"]; + + if (!actionsValue.IsArray()) { + return sets; + } + + for (const auto &action : actionsValue.GetArray()) { + JSONCheermoteSet set; + bool res = ParseSingleCheermoteSet(set, action); + + if (res) { + sets.emplace_back(set); + } + } + + return sets; +} +} // namespace chatterino diff --git a/src/providers/twitch/TwitchParseCheerEmotes.hpp b/src/providers/twitch/TwitchParseCheerEmotes.hpp new file mode 100644 index 000000000..284f87bf0 --- /dev/null +++ b/src/providers/twitch/TwitchParseCheerEmotes.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include "messages/Image.hpp" + +namespace chatterino { + +struct JSONCheermoteSet { + QString prefix; + std::vector scales; + + std::vector backgrounds; + std::vector states; + + QString type; + QString updatedAt; + int priority; + + struct CheermoteTier { + int minBits; + QString id; + QString color; + + // Background State Scale + std::map>> + images; + }; + + std::vector tiers; +}; + +std::vector ParseCheermoteSets(const rapidjson::Document &d); + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchServer.cpp b/src/providers/twitch/TwitchServer.cpp index 247d28fe1..4e3bcddad 100644 --- a/src/providers/twitch/TwitchServer.cpp +++ b/src/providers/twitch/TwitchServer.cpp @@ -28,26 +28,30 @@ TwitchServer::TwitchServer() qDebug() << "init TwitchServer"; this->pubsub = new PubSub; + + // getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) { + // this->connect(); }, + // this->signalHolder_, + // false); } -void TwitchServer::initialize(Application &app) +void TwitchServer::initialize(Settings &settings, Paths &paths) { - this->app = &app; - - app.accounts->twitch.currentUserChanged.connect( + getApp()->accounts->twitch.currentUserChanged.connect( [this]() { postToThread([this] { this->connect(); }); }); } -void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) +void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, + bool isWrite) { - assert(this->app); + this->singleConnection_ = isRead == isWrite; - std::shared_ptr account = getApp()->accounts->twitch.getCurrent(); + std::shared_ptr account = + getApp()->accounts->twitch.getCurrent(); qDebug() << "logging in as" << account->getUserName(); QString username = account->getUserName(); - // QString oauthClient = account->getOAuthClient(); QString oauthToken = account->getOAuthToken(); if (!oauthToken.startsWith("oauth:")) { @@ -60,14 +64,14 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, if (!account->isAnon()) { connection->setPassword(oauthToken); - - // fourtf: ignored users - // this->refreshIgnoredUsers(username, oauthClient, oauthToken); } - connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership")); - connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/commands")); - connection->sendCommand(Communi::IrcCommand::createCapability("REQ", "twitch.tv/tags")); + connection->sendCommand( + Communi::IrcCommand::createCapability("REQ", "twitch.tv/membership")); + connection->sendCommand( + Communi::IrcCommand::createCapability("REQ", "twitch.tv/commands")); + connection->sendCommand( + Communi::IrcCommand::createCapability("REQ", "twitch.tv/tags")); connection->setHost("irc.chat.twitch.tv"); connection->setPort(6667); @@ -75,11 +79,14 @@ void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, std::shared_ptr TwitchServer::createChannel(const QString &channelName) { - TwitchChannel *channel = new TwitchChannel(channelName, this->getReadConnection()); + auto channel = + std::shared_ptr(new TwitchChannel(channelName)); + channel->refreshChannelEmotes(); - channel->sendMessageSignal.connect([this, channel](auto &chan, auto &msg, bool &sent) { - this->onMessageSendRequested(channel, msg, sent); - }); + channel->sendMessageSignal.connect( + [this, channel = channel.get()](auto &chan, auto &msg, bool &sent) { + this->onMessageSendRequested(channel, msg, sent); + }); return std::shared_ptr(channel); } @@ -114,7 +121,8 @@ void TwitchServer::messageReceived(Communi::IrcMessage *message) } else if (command == "MODE") { handler.handleModeMessage(message); } else if (command == "NOTICE") { - handler.handleNoticeMessage(static_cast(message)); + handler.handleNoticeMessage( + static_cast(message)); } else if (command == "JOIN") { handler.handleJoinMessage(message); } else if (command == "PART") { @@ -134,7 +142,8 @@ void TwitchServer::writeConnectionMessageReceived(Communi::IrcMessage *message) } } -std::shared_ptr TwitchServer::getCustomChannel(const QString &channelName) +std::shared_ptr TwitchServer::getCustomChannel( + const QString &channelName) { if (channelName == "/whispers") { return this->whispersChannel; @@ -147,7 +156,8 @@ std::shared_ptr TwitchServer::getCustomChannel(const QString &channelNa return nullptr; } -void TwitchServer::forEachChannelAndSpecialChannels(std::function func) +void TwitchServer::forEachChannelAndSpecialChannels( + std::function func) { this->forEachChannel(func); @@ -155,22 +165,19 @@ void TwitchServer::forEachChannelAndSpecialChannels(std::functionmentionsChannel); } -std::shared_ptr TwitchServer::getChannelOrEmptyByID(const QString &channelID) +std::shared_ptr TwitchServer::getChannelOrEmptyByID( + const QString &channelId) { std::lock_guard lock(this->channelMutex); for (const auto &weakChannel : this->channels) { auto channel = weakChannel.lock(); - if (!channel) { - continue; - } + if (!channel) continue; auto twitchChannel = std::dynamic_pointer_cast(channel); - if (!twitchChannel) { - continue; - } + if (!twitchChannel) continue; - if (twitchChannel->roomID == channelID) { + if (twitchChannel->roomId() == channelId) { return twitchChannel; } } @@ -183,8 +190,14 @@ QString TwitchServer::cleanChannelName(const QString &dirtyChannelName) return dirtyChannelName.toLower(); } -void TwitchServer::onMessageSendRequested(TwitchChannel *channel, const QString &message, - bool &sent) +bool TwitchServer::hasSeparateWriteConnection() const +{ + return true; + // return getSettings()->twitchSeperateWriteConnection; +} + +void TwitchServer::onMessageSendRequested(TwitchChannel *channel, + const QString &message, bool &sent) { sent = false; @@ -192,17 +205,19 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel, const QString std::lock_guard guard(this->lastMessageMutex_); // std::queue - auto &lastMessage = - channel->hasModRights() ? this->lastMessageMod_ : this->lastMessagePleb_; + auto &lastMessage = channel->hasModRights() ? this->lastMessageMod_ + : this->lastMessagePleb_; size_t maxMessageCount = channel->hasModRights() ? 99 : 19; auto minMessageOffset = (channel->hasModRights() ? 100ms : 1100ms); auto now = std::chrono::steady_clock::now(); // check if you are sending messages too fast - if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now) { + if (!lastMessage.empty() && + lastMessage.back() + minMessageOffset > now) { if (this->lastErrorTimeSpeed_ + 30s < now) { - auto errorMessage = Message::createSystemMessage("sending messages too fast"); + auto errorMessage = + makeSystemMessage("sending messages too fast"); channel->addMessage(errorMessage); @@ -219,7 +234,8 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel, const QString // check if you are sending too many messages if (lastMessage.size() >= maxMessageCount) { if (this->lastErrorTimeAmount_ + 30s < now) { - auto errorMessage = Message::createSystemMessage("sending too many messages"); + auto errorMessage = + makeSystemMessage("sending too many messages"); channel->addMessage(errorMessage); @@ -231,7 +247,7 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel, const QString lastMessage.push(now); } - this->sendMessage(channel->name, message); + this->sendMessage(channel->getName(), message); sent = true; } diff --git a/src/providers/twitch/TwitchServer.hpp b/src/providers/twitch/TwitchServer.hpp index 4587d0d87..7e204173f 100644 --- a/src/providers/twitch/TwitchServer.hpp +++ b/src/providers/twitch/TwitchServer.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/MutexValue.hpp" +#include "common/Atomic.hpp" #include "common/Singleton.hpp" #include "providers/irc/AbstractIrcServer.hpp" #include "providers/twitch/TwitchAccount.hpp" @@ -12,22 +12,24 @@ namespace chatterino { +class Settings; +class Paths; + class PubSub; -class TwitchServer : public AbstractIrcServer, public Singleton +class TwitchServer final : public AbstractIrcServer, public Singleton { public: TwitchServer(); virtual ~TwitchServer() override = default; - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; - // fourtf: ugh void forEachChannelAndSpecialChannels(std::function func); std::shared_ptr getChannelOrEmptyByID(const QString &channelID); - MutexValue lastUserThatWhisperedMe; + Atomic lastUserThatWhisperedMe; const ChannelPtr whispersChannel; const ChannelPtr mentionsChannel; @@ -36,27 +38,36 @@ public: PubSub *pubsub; protected: - void initializeConnection(IrcConnection *connection, bool isRead, bool isWrite) override; - std::shared_ptr createChannel(const QString &channelName) override; + virtual void initializeConnection(IrcConnection *connection, bool isRead, + bool isWrite) override; + virtual std::shared_ptr createChannel( + const QString &channelName) override; - void privateMessageReceived(Communi::IrcPrivateMessage *message) override; - void messageReceived(Communi::IrcMessage *message) override; - void writeConnectionMessageReceived(Communi::IrcMessage *message) override; + virtual void privateMessageReceived( + Communi::IrcPrivateMessage *message) override; + virtual void messageReceived(Communi::IrcMessage *message) override; + virtual void writeConnectionMessageReceived( + Communi::IrcMessage *message) override; - std::shared_ptr getCustomChannel(const QString &channelname) override; + virtual std::shared_ptr getCustomChannel( + const QString &channelname) override; - QString cleanChannelName(const QString &dirtyChannelName) override; + virtual QString cleanChannelName(const QString &dirtyChannelName) override; + virtual bool hasSeparateWriteConnection() const override; private: - void onMessageSendRequested(TwitchChannel *channel, const QString &message, bool &sent); - - Application *app = nullptr; + void onMessageSendRequested(TwitchChannel *channel, const QString &message, + bool &sent); std::mutex lastMessageMutex_; std::queue lastMessagePleb_; std::queue lastMessageMod_; std::chrono::steady_clock::time_point lastErrorTimeSpeed_; std::chrono::steady_clock::time_point lastErrorTimeAmount_; + + bool singleConnection_ = false; + + pajlada::Signals::SignalHolder signalHolder_; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchUser.hpp b/src/providers/twitch/TwitchUser.hpp index d69c05572..74a9c1252 100644 --- a/src/providers/twitch/TwitchUser.hpp +++ b/src/providers/twitch/TwitchUser.hpp @@ -36,7 +36,8 @@ namespace Settings { template <> struct Deserialize { - static chatterino::TwitchUser get(const rapidjson::Value &value, bool *error = nullptr) + static chatterino::TwitchUser get(const rapidjson::Value &value, + bool *error = nullptr) { using namespace chatterino; @@ -44,7 +45,8 @@ struct Deserialize { if (!value.IsObject()) { PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION("Deserialized rapidjson::Value is wrong type"); + PAJLADA_THROW_EXCEPTION( + "Deserialized rapidjson::Value is wrong type"); return user; } diff --git a/src/singletons/Badges.cpp b/src/singletons/Badges.cpp new file mode 100644 index 000000000..da38def4f --- /dev/null +++ b/src/singletons/Badges.cpp @@ -0,0 +1,9 @@ +#include "Badges.hpp" + +namespace chatterino { + +Badges::Badges() +{ +} + +} // namespace chatterino diff --git a/src/singletons/Badges.hpp b/src/singletons/Badges.hpp new file mode 100644 index 000000000..d049529eb --- /dev/null +++ b/src/singletons/Badges.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "common/Singleton.hpp" +#include "messages/Emote.hpp" + +namespace chatterino { + +class Badges : public Singleton +{ +public: + Badges(); +}; + +} // namespace chatterino diff --git a/src/singletons/Emotes.cpp b/src/singletons/Emotes.cpp index ca4e91677..cfc23f6d4 100644 --- a/src/singletons/Emotes.cpp +++ b/src/singletons/Emotes.cpp @@ -5,19 +5,18 @@ namespace chatterino { -void Emotes::initialize(Application &app) +Emotes::Emotes() { - const auto refreshTwitchEmotes = [this, &app] { - auto currentUser = app.accounts->twitch.getCurrent(); - assert(currentUser); - this->twitch.refresh(currentUser); - }; - app.accounts->twitch.currentUserChanged.connect(refreshTwitchEmotes); - refreshTwitchEmotes(); +} + +void Emotes::initialize(Settings &settings, Paths &paths) +{ + getApp()->accounts->twitch.currentUserChanged.connect( + [] { getApp()->accounts->twitch.getCurrent()->loadEmotes(); }); this->emojis.load(); - this->bttv.loadGlobalEmotes(); - this->ffz.loadGlobalEmotes(); + this->bttv.loadGlobal(); + this->ffz.loadGlobal(); this->gifTimer.initialize(); } diff --git a/src/singletons/Emotes.hpp b/src/singletons/Emotes.hpp index 279992435..e991fe674 100644 --- a/src/singletons/Emotes.hpp +++ b/src/singletons/Emotes.hpp @@ -12,16 +12,21 @@ namespace chatterino { +class Settings; +class Paths; + class Emotes final : public Singleton { public: - virtual void initialize(Application &app) override; + Emotes(); + + virtual void initialize(Settings &settings, Paths &paths) override; bool isIgnoredEmote(const QString &emote); TwitchEmotes twitch; - BTTVEmotes bttv; - FFZEmotes ffz; + BttvEmotes bttv; + FfzEmotes ffz; Emojis emojis; GIFTimer gifTimer; diff --git a/src/singletons/Fonts.cpp b/src/singletons/Fonts.cpp index 2760723e0..69f327285 100644 --- a/src/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -1,12 +1,12 @@ #include "singletons/Fonts.hpp" +#include "Application.hpp" +#include "debug/AssertInGuiThread.hpp" +#include "singletons/WindowManager.hpp" + #include #include -#include "Application.hpp" -#include "WindowManager.hpp" -#include "debug/AssertInGuiThread.hpp" - #ifdef Q_OS_WIN32 #define DEFAULT_FONT_FAMILY "Segoe UI" #define DEFAULT_FONT_SIZE 10 @@ -29,27 +29,30 @@ Fonts::Fonts() this->fontsByType_.resize(size_t(EndType)); } -void Fonts::initialize(Application &app) +void Fonts::initialize(Settings &, Paths &) { - this->chatFontFamily.connect([this, &app](const std::string &, auto) { + this->chatFontFamily.connect([this](const std::string &, auto) { assertInGuiThread(); - if (app.windows) { - app.windows->incGeneration(); - } - for (auto &map : this->fontsByType_) { map.clear(); } this->fontChanged.invoke(); }); - this->chatFontSize.connect([this, &app](const int &, auto) { + this->chatFontSize.connect([this](const int &, auto) { assertInGuiThread(); - if (app.windows) { - app.windows->incGeneration(); + for (auto &map : this->fontsByType_) { + map.clear(); } + this->fontChanged.invoke(); + }); + + getSettings()->boldScale.connect([this](const int &, auto) { + assertInGuiThread(); + + getApp()->windows->incGeneration(); for (auto &map : this->fontsByType_) { map.clear(); @@ -99,16 +102,20 @@ Fonts::FontData Fonts::createFontData(Type type, float scale) {ChatSmall, {0.6f, false, QFont::Normal}}, {ChatMediumSmall, {0.8f, false, QFont::Normal}}, {ChatMedium, {1, false, QFont::Normal}}, - {ChatMediumBold, {1, false, QFont::Medium}}, + {ChatMediumBold, + {1, false, + QFont::Weight(getApp()->settings->boldScale.getValue())}}, {ChatMediumItalic, {1, true, QFont::Normal}}, {ChatLarge, {1.2f, false, QFont::Normal}}, {ChatVeryLarge, {1.4f, false, QFont::Normal}}, }; - + sizeScale[ChatMediumBold] = { + 1, false, QFont::Weight(getApp()->settings->boldScale.getValue())}; auto data = sizeScale[type]; - return FontData(QFont(QString::fromStdString(this->chatFontFamily.getValue()), - int(this->chatFontSize.getValue() * data.scale * scale), data.weight, - data.italic)); + return FontData( + QFont(QString::fromStdString(this->chatFontFamily.getValue()), + int(this->chatFontSize.getValue() * data.scale * scale), + data.weight, data.italic)); } // normal Ui font (use pt size) @@ -121,8 +128,10 @@ Fonts::FontData Fonts::createFontData(Type type, float scale) static std::unordered_map defaultSize{ {Tiny, {8, "Monospace", false, QFont::Normal}}, - {UiMedium, {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, - {UiTabs, {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + {UiMedium, + {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + {UiTabs, + {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, }; UiFontData &data = defaultSize[type]; diff --git a/src/singletons/Fonts.hpp b/src/singletons/Fonts.hpp index 5a7192292..59c6550c3 100644 --- a/src/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -13,12 +13,15 @@ namespace chatterino { +class Settings; +class Paths; + class Fonts final : public Singleton { public: Fonts(); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; // font data gets set in createFontData(...) enum Type : uint8_t { diff --git a/src/singletons/Logging.cpp b/src/singletons/Logging.cpp index 711e64a9e..9d5823564 100644 --- a/src/singletons/Logging.cpp +++ b/src/singletons/Logging.cpp @@ -12,7 +12,7 @@ namespace chatterino { -void Logging::initialize(Application &app) +void Logging::initialize(Settings &settings, Paths &paths) { } @@ -26,8 +26,8 @@ void Logging::addMessage(const QString &channelName, MessagePtr message) if (it == this->loggingChannels_.end()) { auto channel = new LoggingChannel(channelName); channel->addMessage(message); - this->loggingChannels_.emplace(channelName, - std::unique_ptr(std::move(channel))); + this->loggingChannels_.emplace( + channelName, std::unique_ptr(std::move(channel))); } else { it->second->addMessage(message); } diff --git a/src/singletons/Logging.hpp b/src/singletons/Logging.hpp index f23d8c0e6..98c6c8353 100644 --- a/src/singletons/Logging.hpp +++ b/src/singletons/Logging.hpp @@ -18,7 +18,7 @@ class Logging : public Singleton public: Logging() = default; - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; void addMessage(const QString &channelName, MessagePtr message); diff --git a/src/singletons/NativeMessaging.cpp b/src/singletons/NativeMessaging.cpp index 60226433b..cd80f8021 100644 --- a/src/singletons/NativeMessaging.cpp +++ b/src/singletons/NativeMessaging.cpp @@ -1,4 +1,4 @@ -#include "NativeMessaging.hpp" +#include "singletons/NativeMessaging.hpp" #include "Application.hpp" #include "providers/twitch/TwitchServer.hpp" @@ -31,29 +31,13 @@ namespace ipc = boost::interprocess; namespace chatterino { -// fourtf: don't add this class to the application class -NativeMessaging::NativeMessaging() -{ - qDebug() << "init NativeMessagingManager"; -} +void registerNmManifest(Paths &paths, const QString &manifestFilename, + const QString ®istryKeyName, + const QJsonDocument &document); -void NativeMessaging::writeByteArray(QByteArray a) +void registerNmHost(Paths &paths) { - char *data = a.data(); - uint32_t size; - size = a.size(); - std::cout.write(reinterpret_cast(&size), 4); - std::cout.write(data, a.size()); - std::cout.flush(); -} - -void NativeMessaging::registerHost() -{ - auto app = getApp(); - - if (app->paths->isPortable()) { - return; - } + if (paths.isPortable()) return; auto getBaseDocument = [&] { QJsonObject obj; @@ -65,35 +49,20 @@ void NativeMessaging::registerHost() return obj; }; - auto registerManifest = [&](const QString &manifestFilename, const QString ®istryKeyName, - const QJsonDocument &document) { - // save the manifest - QString manifestPath = app->paths->miscDirectory + manifestFilename; - QFile file(manifestPath); - file.open(QIODevice::WriteOnly | QIODevice::Truncate); - file.write(document.toJson()); - file.flush(); - -#ifdef Q_OS_WIN - // clang-format off - QProcess::execute("REG ADD \"" + registryKeyName + "\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f"); -// clang-format on -#endif - }; - // chrome { QJsonDocument document; auto obj = getBaseDocument(); - QJsonArray allowed_origins_arr = {"chrome-extension://" EXTENSION_ID "/"}; + QJsonArray allowed_origins_arr = {"chrome-extension://" EXTENSION_ID + "/"}; obj.insert("allowed_origins", allowed_origins_arr); document.setObject(obj); - registerManifest( - "/native-messaging-manifest-chrome.json", - "HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.chatterino.chatterino", - document); + registerNmManifest(paths, "/native-messaging-manifest-chrome.json", + "HKCU\\Software\\Google\\Chrome\\NativeMessagingHost" + "s\\com.chatterino.chatterino", + document); } // firefox @@ -105,24 +74,43 @@ void NativeMessaging::registerHost() obj.insert("allowed_extensions", allowed_extensions); document.setObject(obj); - registerManifest("/native-messaging-manifest-firefox.json", - "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com.chatterino.chatterino", - document); + registerNmManifest(paths, "/native-messaging-manifest-firefox.json", + "HKCU\\Software\\Mozilla\\NativeMessagingHosts\\com." + "chatterino.chatterino", + document); } } -void NativeMessaging::openGuiMessageQueue() +void registerNmManifest(Paths &paths, const QString &manifestFilename, + const QString ®istryKeyName, + const QJsonDocument &document) { - static ReceiverThread thread; + (void)registryKeyName; - if (thread.isRunning()) { - thread.exit(); - } + // save the manifest + QString manifestPath = paths.miscDirectory + manifestFilename; + QFile file(manifestPath); + file.open(QIODevice::WriteOnly | QIODevice::Truncate); + file.write(document.toJson()); + file.flush(); - thread.start(); +#ifdef Q_OS_WIN + // clang-format off + QProcess::execute("REG ADD \"" + registryKeyName + "\" /ve /t REG_SZ /d \"" + manifestPath + "\" /f"); +// clang-format on +#endif } -void NativeMessaging::sendToGuiProcess(const QByteArray &array) +std::string &getNmQueueName(Paths &paths) +{ + static std::string name = + "chatterino_gui" + paths.applicationFilePathHash.toStdString(); + return name; +} + +// CLIENT + +void NativeMessagingClient::sendMessage(const QByteArray &array) { try { ipc::message_queue messageQueue(ipc::open_only, "chatterino_gui"); @@ -133,22 +121,41 @@ void NativeMessaging::sendToGuiProcess(const QByteArray &array) } } -void NativeMessaging::ReceiverThread::run() +void NativeMessagingClient::writeToCout(const QByteArray &array) +{ + auto *data = array.data(); + auto size = uint32_t(array.size()); + + std::cout.write(reinterpret_cast(&size), 4); + std::cout.write(data, size); + std::cout.flush(); +} + +// SERVER + +void NativeMessagingServer::start() +{ + this->thread.start(); +} + +void NativeMessagingServer::ReceiverThread::run() { ipc::message_queue::remove("chatterino_gui"); - ipc::message_queue messageQueue(ipc::open_or_create, "chatterino_gui", 100, MESSAGE_SIZE); + ipc::message_queue messageQueue(ipc::open_or_create, "chatterino_gui", 100, + MESSAGE_SIZE); while (true) { try { - std::unique_ptr buf(static_cast(malloc(MESSAGE_SIZE))); + std::unique_ptr buf( + static_cast(malloc(MESSAGE_SIZE))); ipc::message_queue::size_type retSize; unsigned int priority; messageQueue.receive(buf.get(), MESSAGE_SIZE, retSize, priority); - QJsonDocument document = - QJsonDocument::fromJson(QByteArray::fromRawData(buf.get(), retSize)); + QJsonDocument document = QJsonDocument::fromJson( + QByteArray::fromRawData(buf.get(), retSize)); this->handleMessage(document.object()); } catch (ipc::interprocess_exception &ex) { @@ -157,7 +164,8 @@ void NativeMessaging::ReceiverThread::run() } } -void NativeMessaging::ReceiverThread::handleMessage(const QJsonObject &root) +void NativeMessagingServer::ReceiverThread::handleMessage( + const QJsonObject &root) { auto app = getApp(); @@ -192,16 +200,18 @@ void NativeMessaging::ReceiverThread::handleMessage(const QJsonObject &root) if (_type == "twitch") { postToThread([=] { if (!name.isEmpty()) { - app->twitch.server->watchingChannel.update( + app->twitch.server->watchingChannel.reset( app->twitch.server->getOrAddChannel(name)); } if (attach) { #ifdef USEWINSDK // if (args.height != -1) { - auto *window = AttachedWindow::get(::GetForegroundWindow(), args); + auto *window = + AttachedWindow::get(::GetForegroundWindow(), args); if (!name.isEmpty()) { - window->setChannel(app->twitch.server->getOrAddChannel(name)); + window->setChannel( + app->twitch.server->getOrAddChannel(name)); } // } // window->show(); @@ -231,11 +241,4 @@ void NativeMessaging::ReceiverThread::handleMessage(const QJsonObject &root) } } -std::string &NativeMessaging::getGuiMessageQueueName() -{ - static std::string name = - "chatterino_gui" + Paths::getInstance()->applicationFilePathHash.toStdString(); - return name; -} - } // namespace chatterino diff --git a/src/singletons/NativeMessaging.hpp b/src/singletons/NativeMessaging.hpp index b9fa7ce6b..79c90ebd2 100644 --- a/src/singletons/NativeMessaging.hpp +++ b/src/singletons/NativeMessaging.hpp @@ -1,16 +1,28 @@ #pragma once -#include "common/Singleton.hpp" - #include +class Application; +class Paths; + namespace chatterino { -class NativeMessaging final + +void registerNmHost(Application &app); +std::string &getNmQueueName(Paths &paths); + +class NativeMessagingClient final { public: - // fourtf: don't add this class to the application class - NativeMessaging(); + void sendMessage(const QByteArray &array); + void writeToCout(const QByteArray &array); +}; +class NativeMessagingServer final +{ +public: + void start(); + +private: class ReceiverThread : public QThread { public: @@ -20,12 +32,7 @@ public: void handleMessage(const QJsonObject &root); }; - void writeByteArray(QByteArray a); - void registerHost(); - void openGuiMessageQueue(); - void sendToGuiProcess(const QByteArray &array); - - static std::string &getGuiMessageQueueName(); + ReceiverThread thread; }; } // namespace chatterino diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index c695e0468..21d1f57ad 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -14,6 +14,8 @@ Paths *Paths::instance = nullptr; Paths::Paths() { + this->instance = this; + this->initAppFilePathHash(); this->initCheckPortable(); @@ -21,20 +23,6 @@ Paths::Paths() this->initSubDirectories(); } -void Paths::initInstance() -{ - assert(!instance); - - instance = new Paths(); -} - -Paths *Paths::getInstance() -{ - assert(instance); - - return instance; -} - bool Paths::createFolder(const QString &folderPath) { return QDir().mkpath(folderPath); @@ -48,8 +36,9 @@ bool Paths::isPortable() void Paths::initAppFilePathHash() { this->applicationFilePathHash = - QCryptographicHash::hash(QCoreApplication::applicationFilePath().toUtf8(), - QCryptographicHash::Sha224) + QCryptographicHash::hash( + QCoreApplication::applicationFilePath().toUtf8(), + QCryptographicHash::Sha224) .toBase64() .mid(0, 32) .replace("+", "-") @@ -58,15 +47,16 @@ void Paths::initAppFilePathHash() void Paths::initCheckPortable() { - this->portable_ = - QFileInfo::exists(combinePath(QCoreApplication::applicationDirPath(), "portable")); + this->portable_ = QFileInfo::exists( + combinePath(QCoreApplication::applicationDirPath(), "portable")); } void Paths::initAppDataDirectory() { assert(this->portable_.is_initialized()); - // Root path = %APPDATA%/Chatterino or the folder that the executable resides in + // Root path = %APPDATA%/Chatterino or the folder that the executable + // resides in this->rootAppDataDirectory = [&]() -> QString { // portable @@ -75,13 +65,15 @@ void Paths::initAppDataDirectory() } // permanent installation - QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString path = + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (path.isEmpty()) { - throw std::runtime_error("Error finding writable location for settings"); + throw std::runtime_error( + "Error finding writable location for settings"); } -// create directory Chatterino2 instead of chatterino on windows because the ladder one is takes by -// chatterino 1 already +// create directory Chatterino2 instead of chatterino on windows because the +// ladder one is takes by chatterino 1 already #ifdef Q_OS_WIN path.replace("chatterino", "Chatterino"); @@ -96,12 +88,15 @@ void Paths::initSubDirectories() // required the app data directory to be set first assert(!this->rootAppDataDirectory.isEmpty()); - // create settings subdirectories and validate that they are created properly + // create settings subdirectories and validate that they are created + // properly auto makePath = [&](const std::string &name) -> QString { - auto path = combinePath(this->rootAppDataDirectory, QString::fromStdString(name)); + auto path = combinePath(this->rootAppDataDirectory, + QString::fromStdString(name)); if (!QDir().mkpath(path)) { - throw std::runtime_error("Error creating appdata path %appdata%/chatterino/" + name); + throw std::runtime_error( + "Error creating appdata path %appdata%/chatterino/" + name); } return path; @@ -116,7 +111,7 @@ void Paths::initSubDirectories() Paths *getPaths() { - return Paths::getInstance(); + return Paths::instance; } } // namespace chatterino diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index e1df46767..071feb737 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -7,14 +7,13 @@ namespace chatterino { class Paths { +public: + static Paths *instance; + Paths(); -public: - static void initInstance(); - static Paths *getInstance(); - - // Root directory for the configuration files. %APPDATA%/chatterino or ExecutablePath for - // portable mode + // Root directory for the configuration files. %APPDATA%/chatterino or + // ExecutablePath for portable mode QString rootAppDataDirectory; // Directory for settings files. Same as /Settings @@ -41,10 +40,9 @@ private: void initAppDataDirectory(); void initSubDirectories(); - static Paths *instance; boost::optional portable_; }; -Paths *getPaths(); +[[deprecated]] Paths *getPaths(); } // namespace chatterino diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index 0180c581b..daec74a5f 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -1,475 +1 @@ -#include "Resources.hpp" -#include "common/UrlFetch.hpp" - -#include -#include - -namespace chatterino { - -namespace { - -inline Image *lli(const char *pixmapPath, qreal scale = 1) -{ - return new Image(new QPixmap(pixmapPath), scale); -} - -template -inline bool ReadValue(const rapidjson::Value &object, const char *key, Type &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.Is()) { - return false; - } - - out = value.Get(); - - return true; -} - -template <> -inline bool ReadValue(const rapidjson::Value &object, const char *key, QString &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsString()) { - return false; - } - - out = value.GetString(); - - return true; -} - -template <> -inline bool ReadValue>(const rapidjson::Value &object, const char *key, - std::vector &out) -{ - if (!object.HasMember(key)) { - return false; - } - - const auto &value = object[key]; - - if (!value.IsArray()) { - return false; - } - - for (const rapidjson::Value &innerValue : value.GetArray()) { - if (!innerValue.IsString()) { - return false; - } - - out.emplace_back(innerValue.GetString()); - } - - return true; -} - -// Parse a single cheermote set (or "action") from the twitch api -inline bool ParseSingleCheermoteSet(Resources::JSONCheermoteSet &set, - const rapidjson::Value &action) -{ - if (!action.IsObject()) { - return false; - } - - if (!ReadValue(action, "prefix", set.prefix)) { - return false; - } - - if (!ReadValue(action, "scales", set.scales)) { - return false; - } - - if (!ReadValue(action, "backgrounds", set.backgrounds)) { - return false; - } - - if (!ReadValue(action, "states", set.states)) { - return false; - } - - if (!ReadValue(action, "type", set.type)) { - return false; - } - - if (!ReadValue(action, "updated_at", set.updatedAt)) { - return false; - } - - if (!ReadValue(action, "priority", set.priority)) { - return false; - } - - // Tiers - if (!action.HasMember("tiers")) { - return false; - } - - const auto &tiersValue = action["tiers"]; - - if (!tiersValue.IsArray()) { - return false; - } - - for (const rapidjson::Value &tierValue : tiersValue.GetArray()) { - Resources::JSONCheermoteSet::CheermoteTier tier; - - if (!tierValue.IsObject()) { - return false; - } - - if (!ReadValue(tierValue, "min_bits", tier.minBits)) { - return false; - } - - if (!ReadValue(tierValue, "id", tier.id)) { - return false; - } - - if (!ReadValue(tierValue, "color", tier.color)) { - return false; - } - - // Images - if (!tierValue.HasMember("images")) { - return false; - } - - const auto &imagesValue = tierValue["images"]; - - if (!imagesValue.IsObject()) { - return false; - } - - // Read images object - for (const auto &imageBackgroundValue : imagesValue.GetObject()) { - QString background = imageBackgroundValue.name.GetString(); - bool backgroundExists = false; - for (const auto &bg : set.backgrounds) { - if (background == bg) { - backgroundExists = true; - break; - } - } - - if (!backgroundExists) { - continue; - } - - const rapidjson::Value &imageBackgroundStates = imageBackgroundValue.value; - if (!imageBackgroundStates.IsObject()) { - continue; - } - - // Read each key which represents a background - for (const auto &imageBackgroundState : imageBackgroundStates.GetObject()) { - QString state = imageBackgroundState.name.GetString(); - bool stateExists = false; - for (const auto &_state : set.states) { - if (state == _state) { - stateExists = true; - break; - } - } - - if (!stateExists) { - continue; - } - - const rapidjson::Value &imageScalesValue = imageBackgroundState.value; - if (!imageScalesValue.IsObject()) { - continue; - } - - // Read each key which represents a scale - for (const auto &imageScaleValue : imageScalesValue.GetObject()) { - QString scale = imageScaleValue.name.GetString(); - bool scaleExists = false; - for (const auto &_scale : set.scales) { - if (scale == _scale) { - scaleExists = true; - break; - } - } - - if (!scaleExists) { - continue; - } - - const rapidjson::Value &imageScaleURLValue = imageScaleValue.value; - if (!imageScaleURLValue.IsString()) { - continue; - } - - QString url = imageScaleURLValue.GetString(); - - bool ok = false; - qreal scaleNumber = scale.toFloat(&ok); - if (!ok) { - continue; - } - - qreal chatterinoScale = 1 / scaleNumber; - - auto image = new Image(url, chatterinoScale); - - // TODO(pajlada): Fill in name and tooltip - tier.images[background][state][scale] = image; - } - } - } - - set.tiers.emplace_back(tier); - } - - return true; -} - -// Look through the results of https://api.twitch.tv/kraken/bits/actions?channel_id=11148817 for -// cheermote sets or "Actions" as they are called in the API -inline void ParseCheermoteSets(std::vector &sets, - const rapidjson::Document &d) -{ - if (!d.IsObject()) { - return; - } - - if (!d.HasMember("actions")) { - return; - } - - const auto &actionsValue = d["actions"]; - - if (!actionsValue.IsArray()) { - return; - } - - for (const auto &action : actionsValue.GetArray()) { - Resources::JSONCheermoteSet set; - bool res = ParseSingleCheermoteSet(set, action); - - if (res) { - sets.emplace_back(set); - } - } -} - -} // namespace -Resources::Resources() - : badgeStaff(lli(":/images/staff_bg.png")) - , badgeAdmin(lli(":/images/admin_bg.png")) - , badgeGlobalModerator(lli(":/images/globalmod_bg.png")) - , badgeModerator(lli(":/images/moderator_bg.png")) - , badgeTurbo(lli(":/images/turbo_bg.png")) - , badgeBroadcaster(lli(":/images/broadcaster_bg.png")) - , badgePremium(lli(":/images/twitchprime_bg.png")) - , badgeVerified(lli(":/images/verified.png", 0.25)) - , badgeSubscriber(lli(":/images/subscriber.png", 0.25)) - , badgeCollapsed(lli(":/images/collapse.png")) - , cheerBadge100000(lli(":/images/cheer100000")) - , cheerBadge10000(lli(":/images/cheer10000")) - , cheerBadge5000(lli(":/images/cheer5000")) - , cheerBadge1000(lli(":/images/cheer1000")) - , cheerBadge100(lli(":/images/cheer100")) - , cheerBadge1(lli(":/images/cheer1")) - , moderationmode_enabled(lli(":/images/moderatormode_enabled")) - , moderationmode_disabled(lli(":/images/moderatormode_disabled")) - , splitHeaderContext(lli(":/images/tool_moreCollapser_off16.png")) - , buttonBan(lli(":/images/button_ban.png", 0.25)) - , buttonTimeout(lli(":/images/button_timeout.png", 0.25)) -{ - this->split.left = QIcon(":/images/split/splitleft.png"); - this->split.right = QIcon(":/images/split/splitright.png"); - this->split.up = QIcon(":/images/split/splitup.png"); - this->split.down = QIcon(":/images/split/splitdown.png"); - this->split.move = QIcon(":/images/split/splitmove.png"); - - this->buttons.ban = QPixmap(":/images/buttons/ban.png"); - this->buttons.unban = QPixmap(":/images/buttons/unban.png"); - this->buttons.mod = QPixmap(":/images/buttons/mod.png"); - this->buttons.unmod = QPixmap(":/images/buttons/unmod.png"); - - qDebug() << "init ResourceManager"; -} - -void Resources::initialize(Application &app) -{ - this->loadDynamicTwitchBadges(); - - this->loadChatterinoBadges(); -} - -Resources::BadgeVersion::BadgeVersion(QJsonObject &&root) - : badgeImage1x(new Image(root.value("image_url_1x").toString())) - , badgeImage2x(new Image(root.value("image_url_2x").toString())) - , badgeImage4x(new Image(root.value("image_url_4x").toString())) - , description(root.value("description").toString().toStdString()) - , title(root.value("title").toString().toStdString()) - , clickAction(root.value("clickAction").toString().toStdString()) - , clickURL(root.value("clickURL").toString().toStdString()) -{ -} - -void Resources::loadChannelData(const QString &roomID, bool bypassCache) -{ - QString url = "https://badges.twitch.tv/v1/badges/channels/" + roomID + "/display?language=en"; - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - - req.onSuccess([this, roomID](auto result) { - auto root = result.parseJson(); - QJsonObject sets = root.value("badge_sets").toObject(); - - Resources::Channel &ch = this->channels[roomID]; - - for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { - QJsonObject versions = it.value().toObject().value("versions").toObject(); - - auto &badgeSet = ch.badgeSets[it.key().toStdString()]; - auto &versionsMap = badgeSet.versions; - - for (auto versionIt = std::begin(versions); versionIt != std::end(versions); - ++versionIt) { - std::string kkey = versionIt.key().toStdString(); - QJsonObject versionObj = versionIt.value().toObject(); - BadgeVersion v(std::move(versionObj)); - versionsMap.emplace(kkey, v); - } - } - - ch.loaded = true; - - return true; - }); - - req.execute(); - - QString cheermoteUrl = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; - auto request = NetworkRequest::twitchRequest(cheermoteUrl); - request.setCaller(QThread::currentThread()); - - request.onSuccess([this, roomID](auto result) { - auto d = result.parseRapidJson(); - Resources::Channel &ch = this->channels[roomID]; - - ParseCheermoteSets(ch.jsonCheermoteSets, d); - - for (auto &set : ch.jsonCheermoteSets) { - CheermoteSet cheermoteSet; - cheermoteSet.regex = QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); - - for (auto &tier : set.tiers) { - Cheermote cheermote; - - cheermote.color = QColor(tier.color); - cheermote.minBits = tier.minBits; - - // TODO(pajlada): We currently hardcode dark here :| - // We will continue to do so for now since we haven't had to - // solve that anywhere else - cheermote.emoteDataAnimated.image1x = tier.images["dark"]["animated"]["1"]; - cheermote.emoteDataAnimated.image2x = tier.images["dark"]["animated"]["2"]; - cheermote.emoteDataAnimated.image3x = tier.images["dark"]["animated"]["4"]; - - cheermote.emoteDataStatic.image1x = tier.images["dark"]["static"]["1"]; - cheermote.emoteDataStatic.image2x = tier.images["dark"]["static"]["2"]; - cheermote.emoteDataStatic.image3x = tier.images["dark"]["static"]["4"]; - - cheermoteSet.cheermotes.emplace_back(cheermote); - } - - std::sort(cheermoteSet.cheermotes.begin(), cheermoteSet.cheermotes.end(), - [](const auto &lhs, const auto &rhs) { - return lhs.minBits < rhs.minBits; // - }); - - ch.cheermoteSets.emplace_back(cheermoteSet); - } - - return true; - }); - - request.execute(); -} - -void Resources::loadDynamicTwitchBadges() -{ - static QString url("https://badges.twitch.tv/v1/badges/global/display?language=en"); - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - req.onSuccess([this](auto result) { - auto root = result.parseJson(); - QJsonObject sets = root.value("badge_sets").toObject(); - for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { - QJsonObject versions = it.value().toObject().value("versions").toObject(); - - auto &badgeSet = this->badgeSets[it.key().toStdString()]; - auto &versionsMap = badgeSet.versions; - - for (auto versionIt = std::begin(versions); versionIt != std::end(versions); - ++versionIt) { - std::string kkey = versionIt.key().toStdString(); - QJsonObject versionObj = versionIt.value().toObject(); - BadgeVersion v(std::move(versionObj)); - versionsMap.emplace(kkey, v); - } - } - - this->dynamicBadgesLoaded = true; - - return true; - }); - - req.execute(); -} - -void Resources::loadChatterinoBadges() -{ - this->chatterinoBadges.clear(); - - static QString url("https://fourtf.com/chatterino/badges.json"); - - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - - req.onSuccess([this](auto result) { - auto root = result.parseJson(); - QJsonArray badgeVariants = root.value("badges").toArray(); - for (QJsonArray::iterator it = badgeVariants.begin(); it != badgeVariants.end(); ++it) { - QJsonObject badgeVariant = it->toObject(); - const std::string badgeVariantTooltip = - badgeVariant.value("tooltip").toString().toStdString(); - const QString &badgeVariantImageURL = badgeVariant.value("image").toString(); - - auto badgeVariantPtr = std::make_shared( - badgeVariantTooltip, new Image(badgeVariantImageURL)); - - QJsonArray badgeVariantUsers = badgeVariant.value("users").toArray(); - - for (QJsonArray::iterator it = badgeVariantUsers.begin(); it != badgeVariantUsers.end(); - ++it) { - const std::string username = it->toString().toStdString(); - this->chatterinoBadges[username] = - std::shared_ptr(badgeVariantPtr); - } - } - - return true; - }); - - req.execute(); -} - -} // namespace chatterino +#include "singletons/Resources.hpp" diff --git a/src/singletons/Resources.hpp b/src/singletons/Resources.hpp index 7327c4da0..57a1b79ad 100644 --- a/src/singletons/Resources.hpp +++ b/src/singletons/Resources.hpp @@ -1,158 +1,3 @@ #pragma once -#include "common/Singleton.hpp" - -#include "common/Emotemap.hpp" - -#include -#include - -#include -#include -#include - -namespace chatterino { - -class Resources : public Singleton -{ -public: - Resources(); - - ~Resources() = delete; - - virtual void initialize(Application &app) override; - - struct { - QIcon left; - QIcon right; - QIcon up; - QIcon down; - QIcon move; - } split; - - struct { - QPixmap ban; - QPixmap unban; - QPixmap mod; - QPixmap unmod; - } buttons; - - Image *badgeStaff; - Image *badgeAdmin; - Image *badgeGlobalModerator; - Image *badgeModerator; - Image *badgeTurbo; - Image *badgeBroadcaster; - Image *badgePremium; - Image *badgeVerified; - Image *badgeSubscriber; - Image *badgeCollapsed; - - Image *cheerBadge100000; - Image *cheerBadge10000; - Image *cheerBadge5000; - Image *cheerBadge1000; - Image *cheerBadge100; - Image *cheerBadge1; - - Image *moderationmode_enabled; - Image *moderationmode_disabled; - - Image *splitHeaderContext; - - std::map cheerBadges; - - struct BadgeVersion { - BadgeVersion() = delete; - - explicit BadgeVersion(QJsonObject &&root); - - Image *badgeImage1x; - Image *badgeImage2x; - Image *badgeImage4x; - std::string description; - std::string title; - std::string clickAction; - std::string clickURL; - }; - - struct BadgeSet { - std::map versions; - }; - - std::map badgeSets; - - bool dynamicBadgesLoaded = false; - - Image *buttonBan; - Image *buttonTimeout; - - struct JSONCheermoteSet { - QString prefix; - std::vector scales; - - std::vector backgrounds; - std::vector states; - - QString type; - QString updatedAt; - int priority; - - struct CheermoteTier { - int minBits; - QString id; - QString color; - - // Background State Scale - std::map>> images; - }; - - std::vector tiers; - }; - - struct Cheermote { - // a Cheermote indicates one tier - QColor color; - int minBits; - - EmoteData emoteDataAnimated; - EmoteData emoteDataStatic; - }; - - struct CheermoteSet { - QRegularExpression regex; - std::vector cheermotes; - }; - - struct Channel { - std::map badgeSets; - std::vector jsonCheermoteSets; - std::vector cheermoteSets; - - bool loaded = false; - }; - - // channelId - std::map channels; - - // Chatterino badges - struct ChatterinoBadge { - ChatterinoBadge(const std::string &_tooltip, Image *_image) - : tooltip(_tooltip) - , image(_image) - { - } - - std::string tooltip; - Image *image; - }; - - // username - std::map> chatterinoBadges; - - void loadChannelData(const QString &roomID, bool bypassCache = false); - void loadDynamicTwitchBadges(); - void loadChatterinoBadges(); -}; - -} // namespace chatterino +#include "autogenerated/ResourcesAutogen.hpp" diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index d2c1718cd..ea5318ab3 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -10,32 +10,26 @@ namespace chatterino { std::vector> _settings; -void _actuallyRegisterSetting(std::weak_ptr setting) +Settings *Settings::instance = nullptr; + +void _actuallyRegisterSetting( + std::weak_ptr setting) { _settings.push_back(setting); } -Settings::Settings() +Settings::Settings(Paths &paths) { - qDebug() << "init SettingManager"; + instance = this; + + QString settingsPath = paths.settingsDirectory + "/settings.json"; + + pajlada::Settings::SettingManager::gLoad(qPrintable(settingsPath)); } Settings &Settings::getInstance() { - static Settings instance; - - return instance; -} - -void Settings::initialize() -{ -} - -void Settings::load() -{ - QString settingsPath = getPaths()->settingsDirectory + "/settings.json"; - - pajlada::Settings::SettingManager::load(qPrintable(settingsPath)); + return *instance; } void Settings::saveSnapshot() @@ -56,7 +50,7 @@ void Settings::saveSnapshot() this->snapshot_.reset(d); - Log("hehe: {}", pajlada::Settings::SettingManager::stringify(*d)); + log("hehe: {}", pajlada::Settings::SettingManager::stringify(*d)); } void Settings::restoreSnapshot() @@ -70,14 +64,14 @@ void Settings::restoreSnapshot() for (const auto &weakSetting : _settings) { auto setting = weakSetting.lock(); if (!setting) { - Log("Error stage 1 of loading"); + log("Error stage 1 of loading"); continue; } const char *path = setting->getPath().c_str(); if (!snapshotObject.HasMember(path)) { - Log("Error stage 2 of loading"); + log("Error stage 2 of loading"); continue; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index fd30642f1..0dc8b6bda 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -1,5 +1,7 @@ #pragma once +#include "Paths.hpp" + #include "common/ChatterinoSetting.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" @@ -10,52 +12,76 @@ namespace chatterino { -void _actuallyRegisterSetting(std::weak_ptr setting); +void _actuallyRegisterSetting( + std::weak_ptr setting); class Settings { - Settings(); + static Settings *instance; public: - static Settings &getInstance(); + Settings(Paths &paths); - void initialize(); - void load(); + static Settings &getInstance(); /// Appearance BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true}; - QStringSetting timestampFormat = {"/appearance/messages/timestampFormat", "h:mm"}; + QStringSetting timestampFormat = {"/appearance/messages/timestampFormat", + "h:mm"}; BoolSetting showBadges = {"/appearance/messages/showBadges", true}; - BoolSetting showLastMessageIndicator = {"/appearance/messages/showLastMessageIndicator", false}; - IntSetting lastMessagePattern = {"/appearance/messages/lastMessagePattern", Qt::VerPattern}; + BoolSetting showLastMessageIndicator = { + "/appearance/messages/showLastMessageIndicator", false}; + IntSetting lastMessagePattern = {"/appearance/messages/lastMessagePattern", + Qt::VerPattern}; BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true}; - BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", false}; - BoolSetting separateMessages = {"/appearance/messages/separateMessages", false}; - // BoolSetting collapseLongMessages = {"/appearance/messages/collapseLongMessages", false}; - IntSetting collpseMessagesMinLines = {"/appearance/messages/collapseMessagesMinLines", 0}; - BoolSetting alternateMessageBackground = {"/appearance/messages/alternateMessageBackground", - false}; + BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", + false}; + BoolSetting separateMessages = {"/appearance/messages/separateMessages", + false}; + // BoolSetting collapseLongMessages = + // {"/appearance/messages/collapseLongMessages", false}; + IntSetting collpseMessagesMinLines = { + "/appearance/messages/collapseMessagesMinLines", 0}; + BoolSetting alternateMessageBackground = { + "/appearance/messages/alternateMessageBackground", false}; IntSetting uiScale = {"/appearance/uiScale", 0}; + IntSetting boldScale = {"/appearance/boldScale", 57}; BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false}; BoolSetting showTabCloseButton = {"/appearance/showTabCloseButton", true}; - BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton", false}; + BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton", + false}; BoolSetting hideUserButton = {"/appearance/hideUserButton", false}; BoolSetting enableSmoothScrolling = {"/appearance/smoothScrolling", true}; - BoolSetting enableSmoothScrollingNewMessages = {"/appearance/smoothScrollingNewMessages", - false}; - // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", false}; + BoolSetting enableSmoothScrollingNewMessages = { + "/appearance/smoothScrollingNewMessages", false}; + BoolSetting enableUsernameBold = {"/appearence/messages/boldUsernames", + false}; + // BoolSetting customizable splitheader + BoolSetting showViewerCount = {"/appearance/splitheader/showViewerCount", + false}; + BoolSetting showTitle = {"/appearance/splitheader/showTitle", false}; + BoolSetting showGame = {"/appearance/splitheader/showGame", false}; + BoolSetting showUptime = {"/appearance/splitheader/showUptime", false}; + + // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", + // false}; /// Behaviour - BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", true}; + BoolSetting allowDuplicateMessages = {"/behaviour/allowDuplicateMessages", + true}; BoolSetting mentionUsersWithAt = {"/behaviour/mentionUsersWithAt", false}; BoolSetting showJoins = {"/behaviour/showJoins", false}; BoolSetting showParts = {"/behaviour/showParts", false}; - FloatSetting mouseScrollMultiplier = {"/behaviour/mouseScrollMultiplier", 1.0}; + FloatSetting mouseScrollMultiplier = {"/behaviour/mouseScrollMultiplier", + 1.0}; + // BoolSetting twitchSeperateWriteConnection = + // {"/behaviour/twitchSeperateWriteConnection", false}; // Auto-completion BoolSetting onlyFetchChattersForSmallerStreamers = { "/behaviour/autocompletion/onlyFetchChattersForSmallerStreamers", true}; - IntSetting smallStreamerLimit = {"/behaviour/autocompletion/smallStreamerLimit", 1000}; + IntSetting smallStreamerLimit = { + "/behaviour/autocompletion/smallStreamerLimit", 1000}; BoolSetting pauseChatHover = {"/behaviour/pauseChatHover", false}; @@ -63,7 +89,8 @@ public: BoolSetting allowCommandsAtEnd = {"/commands/allowCommandsAtEnd", false}; /// Emotes - BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight", false}; + BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight", + false}; BoolSetting enableTwitchEmotes = {"/emotes/enableTwitchEmotes", true}; BoolSetting enableBttvEmotes = {"/emotes/enableBTTVEmotes", true}; BoolSetting enableFfzEmotes = {"/emotes/enableFFZEmotes", true}; @@ -81,22 +108,25 @@ public: /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; - BoolSetting lowercaseLink = {"/links/linkLowercase", true}; + BoolSetting enableLowercaseLink = {"/links/linkLowercase", true}; /// Ignored phrases QStringSetting ignoredPhraseReplace = {"/ignore/ignoredPhraseReplace", "***"}; /// Ingored Users - BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", true}; + BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", + true}; /// Moderation QStringSetting timeoutAction = {"/moderation/timeoutAction", "Disable"}; /// Highlighting // BoolSetting enableHighlights = {"/highlighting/enabled", true}; - BoolSetting enableHighlightsSelf = {"/highlighting/nameIsHighlightKeyword", true}; + BoolSetting enableHighlightsSelf = {"/highlighting/nameIsHighlightKeyword", + true}; BoolSetting enableHighlightSound = {"/highlighting/enableSound", true}; - BoolSetting enableHighlightTaskbar = {"/highlighting/enableTaskbarFlashing", true}; + BoolSetting enableHighlightTaskbar = {"/highlighting/enableTaskbarFlashing", + true}; BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false}; /// Logging @@ -107,17 +137,18 @@ public: QStringSetting pathHighlightSound = {"/highlighting/highlightSoundPath", "qrc:/sounds/ping2.wav"}; - BoolSetting highlightAlwaysPlaySound = {"/highlighting/alwaysPlaySound", false}; + BoolSetting highlightAlwaysPlaySound = {"/highlighting/alwaysPlaySound", + false}; BoolSetting inlineWhispers = {"/whispers/enableInlineWhispers", true}; - BoolSetting usernameBold = {"/appearence/messages/boldUsernames", false}; - /// External tools // Streamlink - BoolSetting streamlinkUseCustomPath = {"/external/streamlink/useCustomPath", false}; + BoolSetting streamlinkUseCustomPath = {"/external/streamlink/useCustomPath", + false}; QStringSetting streamlinkPath = {"/external/streamlink/customPath", ""}; - QStringSetting preferredQuality = {"/external/streamlink/quality", "Choose"}; + QStringSetting preferredQuality = {"/external/streamlink/quality", + "Choose"}; QStringSetting streamlinkOpts = {"/external/streamlink/options", ""}; /// Misc diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 38c864c36..c5aa16b71 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -1,6 +1,6 @@ #define LOOKUP_COLOR_COUNT 360 -#include "Theme.hpp" +#include "singletons/Theme.hpp" #include @@ -39,7 +39,8 @@ Theme::Theme() void Theme::update() { - this->actuallyUpdate(this->themeHue, detail::getMultiplierByTheme(this->themeName.getValue())); + this->actuallyUpdate(this->themeHue, detail::getMultiplierByTheme( + this->themeName.getValue())); } // hue: theme color (0 - 1) @@ -88,32 +89,40 @@ void Theme::actuallyUpdate(double hue, double multiplier) /// TABS if (lightWin) { - this->tabs.regular = {QColor("#444"), - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {QColor("#fff"), QColor("#fff"), QColor("#fff")}}; - this->tabs.newMessage = {QColor("#222"), - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}}; - this->tabs.highlighted = {fg, - {QColor("#fff"), QColor("#eee"), QColor("#fff")}, - {highlighted, highlighted, highlighted}}; - this->tabs.selected = {QColor("#000"), - {QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, - {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; + this->tabs.regular = { + QColor("#444"), + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {QColor("#fff"), QColor("#fff"), QColor("#fff")}}; + this->tabs.newMessage = { + QColor("#222"), + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}}; + this->tabs.highlighted = { + fg, + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {highlighted, highlighted, highlighted}}; + this->tabs.selected = { + QColor("#000"), + {QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; } else { - this->tabs.regular = {QColor("#aaa"), - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {QColor("#444"), QColor("#444"), QColor("#444")}}; - this->tabs.newMessage = {fg, - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {QColor("#888"), QColor("#888"), QColor("#888")}}; - this->tabs.highlighted = {fg, - {QColor("#252525"), QColor("#252525"), QColor("#252525")}, - {highlighted, highlighted, highlighted}}; + this->tabs.regular = { + QColor("#aaa"), + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#444"), QColor("#444"), QColor("#444")}}; + this->tabs.newMessage = { + fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#888"), QColor("#888"), QColor("#888")}}; + this->tabs.highlighted = { + fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {highlighted, highlighted, highlighted}}; - this->tabs.selected = {QColor("#fff"), - {QColor("#555555"), QColor("#555555"), QColor("#555555")}, - {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; + this->tabs.selected = { + QColor("#fff"), + {QColor("#555555"), QColor("#555555"), QColor("#555555")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; } this->splits.input.focusedLine = highlighted; @@ -126,16 +135,19 @@ void Theme::actuallyUpdate(double hue, double multiplier) // fg, // {QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), // QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), - // QBrush(blendColors(themeColorNoSat, "#ccc", 0.9), Qt::FDiagPattern)}}; + // QBrush(blendColors(themeColorNoSat, "#ccc", 0.9), + // Qt::FDiagPattern)}}; // this->tabs.newMessage = { // fg, - // {QBrush(blendColors(themeColor, "#666", 0.7), Qt::FDiagPattern), - // QBrush(blendColors(themeColor, "#666", 0.5), Qt::FDiagPattern), + // {QBrush(blendColors(themeColor, "#666", 0.7), + // Qt::FDiagPattern), + // QBrush(blendColors(themeColor, "#666", 0.5), + // Qt::FDiagPattern), // QBrush(blendColors(themeColorNoSat, "#666", 0.7), // Qt::FDiagPattern)}}; - // this->tabs.highlighted = {fg, {QColor("#777"), QColor("#777"), - // QColor("#666")}}; + // this->tabs.highlighted = {fg, {QColor("#777"), + // QColor("#777"), QColor("#666")}}; this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color(); } @@ -143,7 +155,8 @@ void Theme::actuallyUpdate(double hue, double multiplier) // Split bool flat = isLight_; - this->splits.messageSeperator = isLight_ ? QColor(127, 127, 127) : QColor(60, 60, 60); + this->splits.messageSeperator = + isLight_ ? QColor(127, 127, 127) : QColor(60, 60, 60); this->splits.background = getColor(0, sat, 1); this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); @@ -165,20 +178,23 @@ void Theme::actuallyUpdate(double hue, double multiplier) 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; - this->splits.header.focusedText = isLight_ ? QColor("#198CFF") : QColor("#84C1FF"); + this->splits.header.focusedText = + isLight_ ? QColor("#198CFF") : QColor("#84C1FF"); this->splits.input.background = getColor(0, sat, flat ? 0.95 : 0.95); this->splits.input.border = getColor(0, sat, flat ? 1 : 1); this->splits.input.text = this->messages.textColors.regular; this->splits.input.styleSheet = "background:" + this->splits.input.background.name() + ";" + - "border:" + this->tabs.selected.backgrounds.regular.color().name() + ";" + - "color:" + this->messages.textColors.regular.name() + ";" + // + "border:" + this->tabs.selected.backgrounds.regular.color().name() + + ";" + "color:" + this->messages.textColors.regular.name() + ";" + // "selection-background-color:" + - (isLight_ ? "#68B1FF" : this->tabs.selected.backgrounds.regular.color().name()); + (isLight_ ? "#68B1FF" + : this->tabs.selected.backgrounds.regular.color().name()); // Message - this->messages.textColors.link = isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244); + this->messages.textColors.link = + isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244); this->messages.textColors.system = QColor(140, 127, 127); this->messages.backgrounds.regular = splits.background; @@ -204,20 +220,22 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->scrollbars.background = QColor(0, 0, 0, 0); // this->scrollbars.background = splits.background; // this->scrollbars.background.setAlphaF(qreal(0.2)); - this->scrollbars.thumb = getColor(0, sat, 0.80); - this->scrollbars.thumbSelected = getColor(0, sat, 0.7); + this->scrollbars.thumb = getColor(0, sat, 0.70); + this->scrollbars.thumbSelected = getColor(0, sat, 0.65); // tooltip this->tooltip.background = QColor(0, 0, 0); this->tooltip.text = QColor(255, 255, 255); // Selection - this->messages.selection = isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); + this->messages.selection = + isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); this->updated.invoke(); } -QColor Theme::blendColors(const QColor &color1, const QColor &color2, qreal ratio) +QColor Theme::blendColors(const QColor &color1, const QColor &color2, + qreal ratio) { int r = int(color1.red() * (1 - ratio) + color2.red() * ratio); int g = int(color1.green() * (1 - ratio) + color2.green() * ratio); @@ -233,21 +251,24 @@ void Theme::normalizeColor(QColor &color) color.setHslF(color.hueF(), color.saturationF(), 0.5); } - if (color.lightnessF() > 0.4 && color.hueF() > 0.1 && color.hueF() < 0.33333) { - color.setHslF( - color.hueF(), color.saturationF(), - color.lightnessF() - sin((color.hueF() - 0.1) / (0.3333 - 0.1) * 3.14159) * - color.saturationF() * 0.4); + if (color.lightnessF() > 0.4 && color.hueF() > 0.1 && + color.hueF() < 0.33333) { + color.setHslF(color.hueF(), color.saturationF(), + color.lightnessF() - sin((color.hueF() - 0.1) / + (0.3333 - 0.1) * 3.14159) * + color.saturationF() * 0.4); } } else { if (color.lightnessF() < 0.5) { color.setHslF(color.hueF(), color.saturationF(), 0.5); } - if (color.lightnessF() < 0.6 && color.hueF() > 0.54444 && color.hueF() < 0.83333) { + if (color.lightnessF() < 0.6 && color.hueF() > 0.54444 && + color.hueF() < 0.83333) { color.setHslF( color.hueF(), color.saturationF(), - color.lightnessF() + sin((color.hueF() - 0.54444) / (0.8333 - 0.54444) * 3.14159) * + color.lightnessF() + sin((color.hueF() - 0.54444) / + (0.8333 - 0.54444) * 3.14159) * color.saturationF() * 0.4); } } diff --git a/src/singletons/Theme.hpp b/src/singletons/Theme.hpp index 25a978271..5ee6f6d58 100644 --- a/src/singletons/Theme.hpp +++ b/src/singletons/Theme.hpp @@ -1,7 +1,7 @@ #pragma once -#include "common/SerializeCustom.hpp" #include "common/Singleton.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include #include @@ -135,8 +135,8 @@ public: private: void actuallyUpdate(double hue, double multiplier); QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio); - void fillLookupTableValues(double (&array)[360], double from, double to, double fromValue, - double toValue); + void fillLookupTableValues(double (&array)[360], double from, double to, + double fromValue, double toValue); double middleLookupTable_[360] = {}; double minLookupTable_[360] = {}; diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 2a525106f..d56a8f510 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -44,10 +44,11 @@ void Updates::installUpdates() } #ifdef Q_OS_WIN - QMessageBox *box = new QMessageBox(QMessageBox::Information, "Chatterino Update", - "Chatterino is downloading the update " - "in the background and will run the " - "updater once it is finished."); + QMessageBox *box = + new QMessageBox(QMessageBox::Information, "Chatterino Update", + "Chatterino is downloading the update " + "in the background and will run the " + "updater once it is finished."); box->setAttribute(Qt::WA_DeleteOnClose); box->show(); @@ -57,8 +58,9 @@ void Updates::installUpdates() this->setStatus_(DownloadFailed); postToThread([] { - QMessageBox *box = new QMessageBox(QMessageBox::Information, "Chatterino Update", - "Failed while trying to download the update."); + QMessageBox *box = + new QMessageBox(QMessageBox::Information, "Chatterino Update", + "Failed while trying to download the update."); box->setAttribute(Qt::WA_DeleteOnClose); box->show(); box->raise(); @@ -67,7 +69,7 @@ void Updates::installUpdates() return true; }); - req.onSuccess([this](auto result) -> bool { + req.onSuccess([this](auto result) -> Outcome { QByteArray object = result.getData(); auto filename = combinePath(getPaths()->miscDirectory, "update.zip"); @@ -76,15 +78,16 @@ void Updates::installUpdates() if (file.write(object) == -1) { this->setStatus_(WriteFileFailed); - return false; + return Failure; } QProcess::startDetached( - combinePath(QCoreApplication::applicationDirPath(), "updater.1/ChatterinoUpdater.exe"), + combinePath(QCoreApplication::applicationDirPath(), + "updater.1/ChatterinoUpdater.exe"), {filename, "restart"}); QApplication::exit(0); - return false; + return Success; }); this->setStatus_(Downloading); req.execute(); @@ -94,11 +97,13 @@ void Updates::installUpdates() void Updates::checkForUpdates() { #ifdef Q_OS_WIN - QString url = "https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS "/stable"; + QString url = + "https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS + "/stable"; NetworkRequest req(url); req.setTimeout(30000); - req.onSuccess([this](auto result) -> bool { + req.onSuccess([this](auto result) -> Outcome { auto object = result.parseJson(); QJsonValue version_val = object.value("version"); QJsonValue update_val = object.value("update"); @@ -110,13 +115,14 @@ void Updates::checkForUpdates() postToThread([] { QMessageBox *box = new QMessageBox( QMessageBox::Information, "Chatterino Update", - "Error while searching for updates.\n\nEither the service is down " + "Error while searching for updates.\n\nEither the service " + "is down " "temporarily or everything is broken."); box->setAttribute(Qt::WA_DeleteOnClose); box->show(); box->raise(); }); - return false; + return Failure; } this->onlineVersion_ = version_val.toString(); @@ -125,11 +131,11 @@ void Updates::checkForUpdates() if (this->currentVersion_ != this->onlineVersion_) { this->setStatus_(UpdateAvailable); postToThread([this] { - QMessageBox *box = - new QMessageBox(QMessageBox::Information, "Chatterino Update", - "An update for chatterino is available.\n\nDo you " - "want to download and install it?", - QMessageBox::Yes | QMessageBox::No); + QMessageBox *box = new QMessageBox( + QMessageBox::Information, "Chatterino Update", + "An update for chatterino is available.\n\nDo you " + "want to download and install it?", + QMessageBox::Yes | QMessageBox::No); box->setAttribute(Qt::WA_DeleteOnClose); box->show(); box->raise(); @@ -140,7 +146,7 @@ void Updates::checkForUpdates() } else { this->setStatus_(NoUpdateAvailable); } - return false; + return Failure; }); this->setStatus_(Searching); req.execute(); diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 4077200df..8ff34c7bf 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -1,4 +1,4 @@ -#include "WindowManager.hpp" +#include "singletons/WindowManager.hpp" #include "Application.hpp" #include "debug/AssertInGuiThread.hpp" @@ -11,12 +11,11 @@ #include "widgets/AccountSwitchPopupWidget.hpp" #include "widgets/dialogs/SettingsDialog.hpp" +#include #include #include #include -#include - #define SETTINGS_FILENAME "/window-layout.json" namespace chatterino { @@ -68,51 +67,61 @@ WindowManager::WindowManager() this->wordFlagsListener_.addSetting(settings->enableEmojis); this->wordFlagsListener_.addSetting(settings->enableFfzEmotes); this->wordFlagsListener_.addSetting(settings->enableTwitchEmotes); + this->wordFlagsListener_.addSetting(settings->enableUsernameBold); + this->wordFlagsListener_.addSetting(settings->enableLowercaseLink); this->wordFlagsListener_.cb = [this](auto) { this->updateWordTypeMask(); // }; } -MessageElement::Flags WindowManager::getWordFlags() +MessageElementFlags WindowManager::getWordFlags() { return this->wordFlags_; } void WindowManager::updateWordTypeMask() { - using MEF = MessageElement::Flags; + using MEF = MessageElementFlag; auto settings = getSettings(); // text - auto flags = MEF::Text | MEF::Text; + auto flags = MessageElementFlags(MEF::Text); // timestamp if (settings->showTimestamps) { - flags |= MEF::Timestamp; + flags.set(MEF::Timestamp); } // emotes - flags |= settings->enableTwitchEmotes ? MEF::TwitchEmoteImage : MEF::TwitchEmoteText; - flags |= settings->enableFfzEmotes ? MEF::FfzEmoteImage : MEF::FfzEmoteText; - flags |= settings->enableBttvEmotes ? MEF::BttvEmoteImage : MEF::BttvEmoteText; - flags |= settings->enableEmojis ? MEF::EmojiImage : MEF::EmojiText; + flags.set(settings->enableTwitchEmotes ? MEF::TwitchEmoteImage + : MEF::TwitchEmoteText); + flags.set(settings->enableFfzEmotes ? MEF::FfzEmoteImage + : MEF::FfzEmoteText); + flags.set(settings->enableBttvEmotes ? MEF::BttvEmoteImage + : MEF::BttvEmoteText); + flags.set(settings->enableEmojis ? MEF::EmojiImage : MEF::EmojiText); // bits - flags |= MEF::BitsAmount; - flags |= settings->enableGifAnimations ? MEF::BitsAnimated : MEF::BitsStatic; + flags.set(MEF::BitsAmount); + flags.set(settings->enableGifAnimations ? MEF::BitsAnimated + : MEF::BitsStatic); // badges - flags |= settings->showBadges ? MEF::Badges : MEF::None; + flags.set(settings->showBadges ? MEF::Badges : MEF::None); // username - flags |= MEF::Username; + flags.set(MEF::Username); // misc - flags |= MEF::AlwaysShow; - flags |= MEF::Collapsed; + flags.set(MEF::AlwaysShow); + flags.set(MEF::Collapsed); + flags.set(settings->enableUsernameBold ? MEF::BoldUsername + : MEF::NonBoldUsername); + flags.set(settings->enableLowercaseLink ? MEF::LowercaseLink + : MEF::OriginalLink); // update flags - MessageElement::Flags newFlags = static_cast(flags); + MessageElementFlags newFlags = static_cast(flags); if (newFlags != this->wordFlags_) { this->wordFlags_ = newFlags; @@ -177,7 +186,8 @@ Window &WindowManager::createWindow(Window::Type type) window->setAttribute(Qt::WA_DeleteOnClose); QObject::connect(window, &QWidget::destroyed, [this, window] { - for (auto it = this->windows_.begin(); it != this->windows_.end(); it++) { + for (auto it = this->windows_.begin(); it != this->windows_.end(); + it++) { if (*it == window) { this->windows_.erase(it); break; @@ -201,16 +211,17 @@ Window *WindowManager::windowAt(int index) if (index < 0 || (size_t)index >= this->windows_.size()) { return nullptr; } - Log("getting window at bad index {}", index); + log("getting window at bad index {}", index); return this->windows_.at(index); } -void WindowManager::initialize(Application &app) +void WindowManager::initialize(Settings &settings, Paths &paths) { assertInGuiThread(); - app.themes->repaintVisibleChatWidgets_.connect([this] { this->repaintVisibleChatWidgets(); }); + getApp()->themes->repaintVisibleChatWidgets_.connect( + [this] { this->repaintVisibleChatWidgets(); }); assert(!this->initialized_); @@ -228,7 +239,8 @@ void WindowManager::initialize(Application &app) // get type QString type_val = window_obj.value("type").toString(); - Window::Type type = type_val == "main" ? Window::Type::Main : Window::Type::Popup; + Window::Type type = + type_val == "main" ? Window::Type::Main : Window::Type::Popup; if (type == Window::Type::Main && mainWindow_ != nullptr) { type = Window::Type::Popup; @@ -300,20 +312,19 @@ void WindowManager::initialize(Application &app) mainWindow_->getNotebook().addPage(true); } - auto settings = getSettings(); + settings.timestampFormat.connect( + [this](auto, auto) { this->layoutChannelViews(); }); - settings->timestampFormat.connect([this](auto, auto) { - auto app = getApp(); - this->layoutChannelViews(); - }); - - settings->emoteScale.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - - settings->timestampFormat.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->alternateMessageBackground.connect( + settings.emoteScale.connect( [this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->separateMessages.connect([this](auto, auto) { this->forceLayoutChannelViews(); }); - settings->collpseMessagesMinLines.connect( + + settings.timestampFormat.connect( + [this](auto, auto) { this->forceLayoutChannelViews(); }); + settings.alternateMessageBackground.connect( + [this](auto, auto) { this->forceLayoutChannelViews(); }); + settings.separateMessages.connect( + [this](auto, auto) { this->forceLayoutChannelViews(); }); + settings.collpseMessagesMinLines.connect( [this](auto, auto) { this->forceLayoutChannelViews(); }); this->initialized_ = true; @@ -353,10 +364,11 @@ void WindowManager::save() // window tabs QJsonArray tabs_arr; - for (int tab_i = 0; tab_i < window->getNotebook().getPageCount(); tab_i++) { + for (int tab_i = 0; tab_i < window->getNotebook().getPageCount(); + tab_i++) { QJsonObject tab_obj; - SplitContainer *tab = - dynamic_cast(window->getNotebook().getPageAt(tab_i)); + SplitContainer *tab = dynamic_cast( + window->getNotebook().getPageAt(tab_i)); assert(tab != nullptr); // custom tab title @@ -416,8 +428,9 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj) } break; case SplitNode::HorizontalContainer: case SplitNode::VerticalContainer: { - obj.insert("type", node->getType() == SplitNode::HorizontalContainer ? "horizontal" - : "vertical"); + obj.insert("type", node->getType() == SplitNode::HorizontalContainer + ? "horizontal" + : "vertical"); QJsonArray items_arr; for (const std::unique_ptr &n : node->getChildren()) { @@ -437,7 +450,7 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) switch (channel.getType()) { case Channel::Type::Twitch: { obj.insert("type", "twitch"); - obj.insert("name", channel.get()->name); + obj.insert("name", channel.get()->getName()); } break; case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); @@ -459,7 +472,8 @@ IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj) QString type = obj.value("type").toString(); if (type == "twitch") { - return app->twitch.server->getOrAddChannel(obj.value("name").toString()); + return app->twitch.server->getOrAddChannel( + obj.value("name").toString()); } else if (type == "mentions") { return app->twitch.server->mentionsChannel; } else if (type == "watching") { diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 217bf737d..eef2be2f1 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -1,13 +1,15 @@ #pragma once -#include - +#include "common/Singleton.hpp" #include "widgets/Window.hpp" #include "widgets/splits/SplitContainer.hpp" namespace chatterino { -class WindowManager : public Singleton +class Settings; +class Paths; + +class WindowManager final : public Singleton { public: WindowManager(); @@ -37,14 +39,14 @@ public: int windowCount(); Window *windowAt(int index); - virtual void initialize(Application &app) override; + virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; void closeAll(); int getGeneration() const; void incGeneration(); - MessageElement::Flags getWordFlags(); + MessageElementFlags getWordFlags(); void updateWordTypeMask(); pajlada::Signals::NoArgSignal repaintGifs; @@ -64,7 +66,7 @@ private: Window *mainWindow_ = nullptr; Window *selectedWindow_ = nullptr; - MessageElement::Flags wordFlags_ = MessageElement::Default; + MessageElementFlags wordFlags_ = MessageElementFlag::Default; pajlada::Settings::SettingListener wordFlagsListener_; }; diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index cce98a505..d70344448 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -21,7 +21,8 @@ LoggingChannel::LoggingChannel(const QString &_channelName) } else if (channelName.startsWith("/mentions")) { this->subDirectory = "Mentions"; } else { - this->subDirectory = QStringLiteral("Channels") + QDir::separator() + channelName; + this->subDirectory = + QStringLiteral("Channels") + QDir::separator() + channelName; } // FOURTF: change this when adding more providers @@ -60,16 +61,17 @@ void LoggingChannel::openLogFile() QString baseFileName = this->channelName + "-" + this->dateString + ".log"; - QString directory = this->baseDirectory + QDir::separator() + this->subDirectory; + QString directory = + this->baseDirectory + QDir::separator() + this->subDirectory; if (!QDir().mkpath(directory)) { - Log("Unable to create logging path"); + log("Unable to create logging path"); return; } // Open file handle to log file of current date QString fileName = directory + QDir::separator() + baseFileName; - Log("Logging to {}", fileName); + log("Logging to {}", fileName); this->fileHandle.setFileName(fileName); this->fileHandle.open(QIODevice::Append); @@ -77,7 +79,7 @@ void LoggingChannel::openLogFile() this->appendLine(this->generateOpeningString(now)); } -void LoggingChannel::addMessage(std::shared_ptr message) +void LoggingChannel::addMessage(MessagePtr message) { QDateTime now = QDateTime::currentDateTime(); diff --git a/src/singletons/helper/LoggingChannel.hpp b/src/singletons/helper/LoggingChannel.hpp index 985ab27b9..1a29a0c6f 100644 --- a/src/singletons/helper/LoggingChannel.hpp +++ b/src/singletons/helper/LoggingChannel.hpp @@ -19,13 +19,15 @@ class LoggingChannel : boost::noncopyable public: ~LoggingChannel(); - void addMessage(std::shared_ptr message); + void addMessage(MessagePtr message); private: void openLogFile(); - QString generateOpeningString(const QDateTime &now = QDateTime::currentDateTime()) const; - QString generateClosingString(const QDateTime &now = QDateTime::currentDateTime()) const; + QString generateOpeningString( + const QDateTime &now = QDateTime::currentDateTime()) const; + QString generateClosingString( + const QDateTime &now = QDateTime::currentDateTime()) const; void appendLine(const QString &line); diff --git a/src/util/ConcurrentMap.hpp b/src/util/ConcurrentMap.hpp index 84cf98365..281f3ce46 100644 --- a/src/util/ConcurrentMap.hpp +++ b/src/util/ConcurrentMap.hpp @@ -65,7 +65,8 @@ public: this->data_.insert(name, value); } - void each(std::function func) const + void each( + std::function func) const { QMutexLocker lock(&this->mutex_); diff --git a/src/util/DebugCount.cpp b/src/util/DebugCount.cpp index 1f6d28a74..81be6250b 100644 --- a/src/util/DebugCount.cpp +++ b/src/util/DebugCount.cpp @@ -2,7 +2,6 @@ namespace chatterino { -QMap DebugCount::counts_; -std::mutex DebugCount::mut_; +UniqueAccess> DebugCount::counts_; } // namespace chatterino diff --git a/src/util/DebugCount.hpp b/src/util/DebugCount.hpp index 7e78641d1..46bfb83a1 100644 --- a/src/util/DebugCount.hpp +++ b/src/util/DebugCount.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -13,11 +15,11 @@ class DebugCount public: static void increase(const QString &name) { - std::lock_guard lock(mut_); + auto counts = counts_.access(); - auto it = counts_.find(name); - if (it == counts_.end()) { - counts_.insert(name, 1); + auto it = counts->find(name); + if (it == counts->end()) { + counts->insert(name, 1); } else { reinterpret_cast(it.value())++; } @@ -25,11 +27,11 @@ public: static void decrease(const QString &name) { - std::lock_guard lock(mut_); + auto counts = counts_.access(); - auto it = counts_.find(name); - if (it == counts_.end()) { - counts_.insert(name, -1); + auto it = counts->find(name); + if (it == counts->end()) { + counts->insert(name, -1); } else { reinterpret_cast(it.value())--; } @@ -37,10 +39,10 @@ public: static QString getDebugText() { - std::lock_guard lock(mut_); + auto counts = counts_.access(); QString text; - for (auto it = counts_.begin(); it != counts_.end(); it++) { + for (auto it = counts->begin(); it != counts->end(); it++) { text += it.key() + ": " + QString::number(it.value()) + "\n"; } return text; @@ -52,8 +54,7 @@ public: } private: - static QMap counts_; - static std::mutex mut_; + static UniqueAccess> counts_; }; } // namespace chatterino diff --git a/src/util/FormatTime.cpp b/src/util/FormatTime.cpp new file mode 100644 index 000000000..2eccca901 --- /dev/null +++ b/src/util/FormatTime.cpp @@ -0,0 +1,46 @@ +#include "FormatTime.hpp" + +namespace chatterino { +namespace { +void appendDuration(int count, QChar &&order, QString &outString) +{ + outString.append(QString::number(count)); + outString.append(order); +} +} // namespace + +QString formatTime(int totalSeconds) +{ + QString res; + + int seconds = totalSeconds % 60; + int timeoutMinutes = totalSeconds / 60; + int minutes = timeoutMinutes % 60; + int timeoutHours = timeoutMinutes / 60; + int hours = timeoutHours % 24; + int days = timeoutHours / 24; + if (days > 0) { + appendDuration(days, 'd', res); + } + if (hours > 0) { + if (!res.isEmpty()) { + res.append(" "); + } + appendDuration(hours, 'h', res); + } + if (minutes > 0) { + if (!res.isEmpty()) { + res.append(" "); + } + appendDuration(minutes, 'm', res); + } + if (seconds > 0) { + if (!res.isEmpty()) { + res.append(" "); + } + appendDuration(seconds, 's', res); + } + return res; +} + +} // namespace chatterino diff --git a/src/util/FormatTime.hpp b/src/util/FormatTime.hpp new file mode 100644 index 000000000..72d45e51f --- /dev/null +++ b/src/util/FormatTime.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace chatterino { + +// format: 1h 23m 42s +QString formatTime(int totalSeconds); + +} // namespace chatterino diff --git a/src/util/FunctionEventFilter.cpp b/src/util/FunctionEventFilter.cpp new file mode 100644 index 000000000..3006597b2 --- /dev/null +++ b/src/util/FunctionEventFilter.cpp @@ -0,0 +1,17 @@ +#include "FunctionEventFilter.hpp" + +namespace chatterino { + +FunctionEventFilter::FunctionEventFilter( + QObject *parent, std::function function) + : QObject(parent) + , function_(std::move(function)) +{ +} + +bool FunctionEventFilter::eventFilter(QObject *watched, QEvent *event) +{ + return this->function_(watched, event); +} + +} // namespace chatterino diff --git a/src/util/FunctionEventFilter.hpp b/src/util/FunctionEventFilter.hpp new file mode 100644 index 000000000..a849af6f0 --- /dev/null +++ b/src/util/FunctionEventFilter.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace chatterino { + +class FunctionEventFilter : public QObject +{ + Q_OBJECT + +public: + FunctionEventFilter(QObject *parent, + std::function function); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + std::function function_; +}; + +} // namespace chatterino diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index b708728b1..1552ae404 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -6,7 +6,7 @@ namespace chatterino { template -auto fS(Args &&... args) -> decltype(fmt::format(std::forward(args)...)) +auto fS(Args &&... args) { return fmt::format(std::forward(args)...); } diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index acb584175..25d131635 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -1,22 +1,24 @@ #include "InitUpdateButton.hpp" #include "widgets/dialogs/UpdateDialog.hpp" -#include "widgets/helper/RippleEffectButton.hpp" +#include "widgets/helper/Button.hpp" namespace chatterino { -void initUpdateButton(RippleEffectButton &button, std::unique_ptr &handle, +void initUpdateButton(Button &button, + std::unique_ptr &handle, pajlada::Signals::SignalHolder &signalHolder) { button.hide(); // show update prompt when clicking the button - QObject::connect(&button, &RippleEffectButton::clicked, [&button, &handle] { + QObject::connect(&button, &Button::clicked, [&button, &handle] { (void)(handle); auto dialog = new UpdateDialog(); dialog->setActionOnFocusLoss(BaseWindow::Delete); - dialog->move(button.mapToGlobal(QPoint(int(-100 * button.getScale()), button.height()))); + dialog->move(button.mapToGlobal( + QPoint(int(-100 * button.getScale()), button.height()))); dialog->show(); dialog->raise(); @@ -39,15 +41,17 @@ void initUpdateButton(RippleEffectButton &button, std::unique_ptr auto updateChange = [&button](auto) { button.setVisible(Updates::getInstance().shouldShowUpdateButton()); - auto imageUrl = Updates::getInstance().isError() ? ":/images/download_update_error.png" - : ":/images/download_update.png"; + auto imageUrl = Updates::getInstance().isError() + ? ":/images/download_update_error.png" + : ":/images/download_update.png"; button.setPixmap(QPixmap(imageUrl)); }; updateChange(Updates::getInstance().getStatus()); - signalHolder.managedConnect(Updates::getInstance().statusUpdated, - [updateChange](auto status) { updateChange(status); }); + signalHolder.managedConnect( + Updates::getInstance().statusUpdated, + [updateChange](auto status) { updateChange(status); }); } } // namespace chatterino diff --git a/src/util/InitUpdateButton.hpp b/src/util/InitUpdateButton.hpp index 6a0966e54..e95fcd7bd 100644 --- a/src/util/InitUpdateButton.hpp +++ b/src/util/InitUpdateButton.hpp @@ -10,10 +10,11 @@ class SignalHolder; namespace chatterino { -class RippleEffectButton; +class Button; class UpdateDialog; -void initUpdateButton(RippleEffectButton &button, std::unique_ptr &handle, +void initUpdateButton(Button &button, + std::unique_ptr &handle, pajlada::Signals::SignalHolder &signalHolder); } // namespace chatterino diff --git a/src/util/JsonQuery.cpp b/src/util/JsonQuery.cpp new file mode 100644 index 000000000..1c7f18a3c --- /dev/null +++ b/src/util/JsonQuery.cpp @@ -0,0 +1,9 @@ +#include "JsonQuery.hpp" + +namespace chatterino { + +JsonQuery::JsonQuery() +{ +} + +} // namespace chatterino diff --git a/src/util/JsonQuery.hpp b/src/util/JsonQuery.hpp new file mode 100644 index 000000000..7c8f4c11f --- /dev/null +++ b/src/util/JsonQuery.hpp @@ -0,0 +1,12 @@ +#pragma once + +class QJsonObject; + +namespace chatterino { +class JsonQuery +{ +public: + JsonQuery(); +}; + +} // namespace chatterino diff --git a/src/util/LayoutCreator.hpp b/src/util/LayoutCreator.hpp index af01db373..c519b7659 100644 --- a/src/util/LayoutCreator.hpp +++ b/src/util/LayoutCreator.hpp @@ -53,7 +53,8 @@ public: } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> LayoutCreator emplaceScrollAreaWidget() { QWidget *widget = new QWidget; @@ -62,8 +63,10 @@ public: } template ::value, int>::type = 0, - typename std::enable_if::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0, + typename std::enable_if::value, + int>::type = 0> LayoutCreator setLayoutType() { T2 *layout = new T2; @@ -81,7 +84,8 @@ public: } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> LayoutCreator withoutMargin() { this->item_->setContentsMargins(0, 0, 0, 0); @@ -90,7 +94,8 @@ public: } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> LayoutCreator hidden() { this->item_->setVisible(false); @@ -99,10 +104,12 @@ public: } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> LayoutCreator appendTab(T2 *item, const QString &title) { - static_assert(std::is_base_of::value, "needs to be QLayout"); + static_assert(std::is_base_of::value, + "needs to be QLayout"); QWidget *widget = new QWidget; widget->setLayout(item); @@ -116,14 +123,16 @@ private: T *item_; template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> void addItem(QLayout *layout, T2 *item) { layout->addWidget(item); } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> void addItem(QLayout *layout, T2 *item) { QWidget *widget = new QWidget(); @@ -132,14 +141,16 @@ private: } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> QLayout *getOrCreateLayout() { return this->item_; } template ::value, int>::type = 0> + typename std::enable_if::value, + int>::type = 0> QLayout *getOrCreateLayout() { if (!this->item_->layout()) { diff --git a/src/util/LayoutHelper.hpp b/src/util/LayoutHelper.hpp new file mode 100644 index 000000000..b996aa084 --- /dev/null +++ b/src/util/LayoutHelper.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace chatterino { + +using LayoutItem = boost::variant; + +template +T *makeLayout(std::initializer_list items) +{ + auto t = new T; + + for (auto &item : items) { + switch (item.which()) { + case 0: + t->addItem(new QWidgetItem(boost::get(item))); + break; + case 1: + t->addItem(boost::get(item)); + break; + } + } + + return t; +} + +template +T *makeWidget(With with) +{ + auto t = new T; + + with(t); + + return t; +} + +} // namespace chatterino diff --git a/src/util/PostToThread.hpp b/src/util/PostToThread.hpp index ddf5caa12..ab0fbe1cb 100644 --- a/src/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -7,7 +7,8 @@ #include -#define async_exec(a) QThreadPool::globalInstance()->start(new LambdaRunnable(a)); +#define async_exec(a) \ + QThreadPool::globalInstance()->start(new LambdaRunnable(a)); namespace chatterino { diff --git a/src/common/SerializeCustom.hpp b/src/util/RapidJsonSerializeQString.hpp similarity index 67% rename from src/common/SerializeCustom.hpp rename to src/util/RapidJsonSerializeQString.hpp index 9aa31bcfb..754cbf115 100644 --- a/src/common/SerializeCustom.hpp +++ b/src/util/RapidJsonSerializeQString.hpp @@ -8,7 +8,8 @@ namespace Settings { template <> struct Serialize { - static rapidjson::Value get(const QString &value, rapidjson::Document::AllocatorType &a) + static rapidjson::Value get(const QString &value, + rapidjson::Document::AllocatorType &a) { return rapidjson::Value(value.toUtf8(), a); } @@ -20,12 +21,14 @@ struct Deserialize { { if (!value.IsString()) { PAJLADA_REPORT_ERROR(error) - PAJLADA_THROW_EXCEPTION("Deserialized rapidjson::Value is not a string"); + PAJLADA_THROW_EXCEPTION( + "Deserialized rapidjson::Value is not a string"); return QString{}; } try { - return QString::fromUtf8(value.GetString(), value.GetStringLength()); + return QString::fromUtf8(value.GetString(), + value.GetStringLength()); } catch (const std::exception &) { // int x = 5; } catch (...) { diff --git a/src/util/RapidjsonHelpers.hpp b/src/util/RapidjsonHelpers.hpp index 25e72ed4b..8dbfe07a2 100644 --- a/src/util/RapidjsonHelpers.hpp +++ b/src/util/RapidjsonHelpers.hpp @@ -1,6 +1,6 @@ #pragma once -#include "common/SerializeCustom.hpp" +#include "util/RapidJsonSerializeQString.hpp" #include #include @@ -26,7 +26,8 @@ void set(rapidjson::Value &obj, const char *key, const Type &value, } template <> -inline void set(rapidjson::Value &obj, const char *key, const rapidjson::Value &value, +inline void set(rapidjson::Value &obj, const char *key, + const rapidjson::Value &value, rapidjson::Document::AllocatorType &a) { assert(obj.IsObject()); @@ -45,7 +46,8 @@ void set(rapidjson::Document &obj, const char *key, const Type &value) } template <> -inline void set(rapidjson::Document &obj, const char *key, const rapidjson::Value &value) +inline void set(rapidjson::Document &obj, const char *key, + const rapidjson::Value &value) { assert(obj.IsObject()); @@ -55,7 +57,8 @@ inline void set(rapidjson::Document &obj, const char *key, const rapidjson::Valu } template -void add(rapidjson::Value &arr, const Type &value, rapidjson::Document::AllocatorType &a) +void add(rapidjson::Value &arr, const Type &value, + rapidjson::Document::AllocatorType &a) { assert(arr.IsArray()); diff --git a/src/util/RemoveScrollAreaBackground.hpp b/src/util/RemoveScrollAreaBackground.hpp index 66629c4e7..b82bbeea2 100644 --- a/src/util/RemoveScrollAreaBackground.hpp +++ b/src/util/RemoveScrollAreaBackground.hpp @@ -4,7 +4,8 @@ namespace chatterino { -static void removeScrollAreaBackground(QScrollArea *scrollArea, QWidget *childWidget) +static void removeScrollAreaBackground(QScrollArea *scrollArea, + QWidget *childWidget) { scrollArea->setWidgetResizable(true); scrollArea->setFrameStyle(0); diff --git a/src/util/SharedPtrElementLess.hpp b/src/util/SharedPtrElementLess.hpp index 0fa121a1d..b88f20230 100644 --- a/src/util/SharedPtrElementLess.hpp +++ b/src/util/SharedPtrElementLess.hpp @@ -6,7 +6,8 @@ namespace chatterino { template struct SharedPtrElementLess { - bool operator()(const std::shared_ptr &a, const std::shared_ptr &b) const + bool operator()(const std::shared_ptr &a, + const std::shared_ptr &b) const { return a->operator<(*b); } diff --git a/src/util/StandardItemHelper.hpp b/src/util/StandardItemHelper.hpp index 860dddc2a..4638e7277 100644 --- a/src/util/StandardItemHelper.hpp +++ b/src/util/StandardItemHelper.hpp @@ -4,19 +4,21 @@ namespace chatterino { -static void setBoolItem(QStandardItem *item, bool value, bool userCheckable = true, - bool selectable = true) +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)( + Qt::ItemIsEnabled | (selectable ? Qt::ItemIsSelectable : 0) | + (userCheckable ? Qt::ItemIsUserCheckable : 0))); item->setCheckState(value ? Qt::Checked : Qt::Unchecked); } -static void setStringItem(QStandardItem *item, const QString &value, bool editable = true, - bool selectable = true) +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)(Qt::ItemIsEnabled | + (selectable ? Qt::ItemIsSelectable : 0) | (editable ? (Qt::ItemIsEditable) : 0))); } diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index 312f027d2..2fd2ef0a3 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -50,7 +50,8 @@ bool checkStreamlinkPath(const QString &path) if (!fileinfo.exists()) { return false; - // throw Exception(fS("Streamlink path ({}) is invalid, file does not exist", path)); + // throw Exception(fS("Streamlink path ({}) is invalid, file does not + // exist", path)); } return fileinfo.isExecutable(); @@ -63,11 +64,13 @@ void showStreamlinkNotFoundError() auto app = getApp(); if (app->settings->streamlinkUseCustomPath) { msg->showMessage( - "Unable to find Streamlink executable\nMake sure your custom path is pointing " + "Unable to find Streamlink executable\nMake sure your custom path " + "is pointing " "to the DIRECTORY where the streamlink executable is located"); } else { - msg->showMessage("Unable to find Streamlink executable.\nIf you have Streamlink " - "installed, you might need to enable the custom path option"); + msg->showMessage( + "Unable to find Streamlink executable.\nIf you have Streamlink " + "installed, you might need to enable the custom path option"); } } @@ -86,51 +89,58 @@ QProcess *createStreamlinkProcess() p->deleteLater(); }); - QObject::connect(p, static_cast(&QProcess::finished), [=](int res) { - p->deleteLater(); // - }); + QObject::connect(p, + static_cast(&QProcess::finished), + [=](int res) { + p->deleteLater(); // + }); return p; } } // namespace -void getStreamQualities(const QString &channelURL, std::function cb) +void getStreamQualities(const QString &channelURL, + std::function cb) { auto p = createStreamlinkProcess(); - QObject::connect(p, static_cast(&QProcess::finished), [=](int res) { - if (res != 0) { - qDebug() << "Got error code" << res; - // return; - } - QString lastLine = QString(p->readAllStandardOutput()); - lastLine = lastLine.trimmed().split('\n').last().trimmed(); - if (lastLine.startsWith("Available streams: ")) { - QStringList options; - QStringList split = lastLine.right(lastLine.length() - 19).split(", "); - - for (int i = split.length() - 1; i >= 0; i--) { - QString option = split.at(i); - if (option.endsWith(" (worst)")) { - options << option.left(option.length() - 8); - } else if (option.endsWith(" (best)")) { - options << option.left(option.length() - 7); - } else { - options << option; - } + QObject::connect( + p, static_cast(&QProcess::finished), + [=](int res) { + if (res != 0) { + qDebug() << "Got error code" << res; + // return; } + QString lastLine = QString(p->readAllStandardOutput()); + lastLine = lastLine.trimmed().split('\n').last().trimmed(); + if (lastLine.startsWith("Available streams: ")) { + QStringList options; + QStringList split = + lastLine.right(lastLine.length() - 19).split(", "); - cb(options); - } - }); + for (int i = split.length() - 1; i >= 0; i--) { + QString option = split.at(i); + if (option.endsWith(" (worst)")) { + options << option.left(option.length() - 8); + } else if (option.endsWith(" (best)")) { + options << option.left(option.length() - 7); + } else { + options << option; + } + } + + cb(options); + } + }); p->setArguments({channelURL, "--default-stream=KKona"}); p->start(); } -void openStreamlink(const QString &channelURL, const QString &quality, QStringList extraArguments) +void openStreamlink(const QString &channelURL, const QString &quality, + QStringList extraArguments) { auto app = getApp(); @@ -149,7 +159,8 @@ void openStreamlink(const QString &channelURL, const QString &quality, QStringLi arguments << quality; } - bool res = QProcess::startDetached(getStreamlinkProgram() + " " + QString(arguments.join(' '))); + bool res = QProcess::startDetached(getStreamlinkProgram() + " " + + QString(arguments.join(' '))); if (!res) { showStreamlinkNotFoundError(); diff --git a/src/util/StreamLink.hpp b/src/util/StreamLink.hpp index 2c5eebf4a..99dbb95b9 100644 --- a/src/util/StreamLink.hpp +++ b/src/util/StreamLink.hpp @@ -15,12 +15,13 @@ public: }; // Open streamlink for given channel, quality and extra arguments -// the "Additional arguments" are fetched and added at the beginning of the streamlink call +// the "Additional arguments" are fetched and added at the beginning of the +// streamlink call void openStreamlink(const QString &channelURL, const QString &quality, QStringList extraArguments = QStringList()); -// Start opening streamlink for the given channel, reading settings like quality from settings -// and opening a quality dialog if the quality is "Choose" +// Start opening streamlink for the given channel, reading settings like quality +// from settings and opening a quality dialog if the quality is "Choose" void openStreamlinkForChannel(const QString &channel); } // namespace chatterino diff --git a/src/util/WindowsHelper.cpp b/src/util/WindowsHelper.cpp index 743362ade..38ca74698 100644 --- a/src/util/WindowsHelper.cpp +++ b/src/util/WindowsHelper.cpp @@ -11,14 +11,17 @@ typedef enum MONITOR_DPI_TYPE { MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE; -typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *, UINT *); +typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *, + UINT *); boost::optional getWindowDpi(HWND hwnd) { static HINSTANCE shcore = LoadLibrary(L"Shcore.dll"); if (shcore != nullptr) { - if (auto getDpiForMonitor = GetDpiForMonitor_(GetProcAddress(shcore, "GetDpiForMonitor"))) { - HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (auto getDpiForMonitor = + GetDpiForMonitor_(GetProcAddress(shcore, "GetDpiForMonitor"))) { + HMONITOR monitor = + MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); UINT xScale, yScale; diff --git a/src/widgets/AccountSwitchWidget.cpp b/src/widgets/AccountSwitchWidget.cpp index 3b2acc268..9c307ba73 100644 --- a/src/widgets/AccountSwitchWidget.cpp +++ b/src/widgets/AccountSwitchWidget.cpp @@ -39,10 +39,12 @@ AccountSwitchWidget::AccountSwitchWidget(QWidget *parent) QObject::connect(this, &QListWidget::clicked, [=] { if (!this->selectedItems().isEmpty()) { QString newUsername = this->currentItem()->text(); - if (newUsername.compare(ANONYMOUS_USERNAME_LABEL, Qt::CaseInsensitive) == 0) { + if (newUsername.compare(ANONYMOUS_USERNAME_LABEL, + Qt::CaseInsensitive) == 0) { app->accounts->twitch.currentUsername = ""; } else { - app->accounts->twitch.currentUsername = newUsername.toStdString(); + app->accounts->twitch.currentUsername = + newUsername.toStdString(); } } }); @@ -70,7 +72,8 @@ void AccountSwitchWidget::refreshSelection() for (int i = 0; i < this->count(); ++i) { QString itemText = this->item(i)->text(); - if (itemText.compare(currentUsername, Qt::CaseInsensitive) == 0) { + if (itemText.compare(currentUsername, Qt::CaseInsensitive) == + 0) { this->setCurrentRow(i); break; } diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 6fdf93680..b247129b6 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -129,14 +129,17 @@ void AttachedWindow::attachToHwnd(void *_attachedPtr) DWORD processId; ::GetWindowThreadProcessId(attached, &processId); - HANDLE process = - ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId); + HANDLE process = ::OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, processId); std::unique_ptr filename(new TCHAR[512]); - DWORD filenameLength = ::GetModuleFileNameEx(process, nullptr, filename.get(), 512); - QString qfilename = QString::fromWCharArray(filename.get(), filenameLength); + DWORD filenameLength = + ::GetModuleFileNameEx(process, nullptr, filename.get(), 512); + QString qfilename = + QString::fromWCharArray(filename.get(), filenameLength); - if (!qfilename.endsWith("chrome.exe") && !qfilename.endsWith("firefox.exe")) { + if (!qfilename.endsWith("chrome.exe") && + !qfilename.endsWith("firefox.exe")) { qDebug() << "NM Illegal caller" << qfilename; this->timer_.stop(); this->deleteLater(); @@ -157,9 +160,9 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) auto hwnd = HWND(this->winId()); auto attached = HWND(_attachedPtr); - // We get the window rect first so we can close this window when it returns an error. - // If we query the process first and check the filename then it will return and empty string - // that doens't match. + // We get the window rect first so we can close this window when it returns + // an error. If we query the process first and check the filename then it + // will return and empty string that doens't match. ::SetLastError(0); RECT rect; ::GetWindowRect(attached, &rect); @@ -189,21 +192,25 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr) } if (this->height_ == -1) { - // ::MoveWindow(hwnd, rect.right - this->width_ - 8, rect.top + this->yOffset_ - 8, - // this->width_, rect.bottom - rect.top - this->yOffset_, false); + // ::MoveWindow(hwnd, rect.right - this->width_ - 8, rect.top + + // this->yOffset_ - 8, + // this->width_, rect.bottom - rect.top - this->yOffset_, + // false); } else { ::MoveWindow(hwnd, // int(rect.right - this->width_ * scale - 8), // int(rect.bottom - this->height_ * scale - 8), // - int(this->width_ * scale), int(this->height_ * scale), true); + int(this->width_ * scale), int(this->height_ * scale), + true); } -// ::MoveWindow(hwnd, rect.right - 360, rect.top + 82, 360 - 8, rect.bottom - -// rect.top - 82 - 8, false); +// ::MoveWindow(hwnd, rect.right - 360, rect.top + 82, 360 - 8, +// rect.bottom - rect.top - 82 - 8, false); #endif } -// void AttachedWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) +// void AttachedWindow::nativeEvent(const QByteArray &eventType, void *message, +// long *result) //{ // MSG *msg = reinterpret_cast diff --git a/src/widgets/AttachedWindow.hpp b/src/widgets/AttachedWindow.hpp index b7a9ad617..80b030fca 100644 --- a/src/widgets/AttachedWindow.hpp +++ b/src/widgets/AttachedWindow.hpp @@ -28,8 +28,8 @@ public: protected: virtual void showEvent(QShowEvent *) override; - // virtual void nativeEvent(const QByteArray &eventType, void *message, long *result) - // override; + // virtual void nativeEvent(const QByteArray &eventType, void *message, + // long *result) override; private: struct { diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index 818d951a8..278fe9248 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -97,12 +97,14 @@ void BaseWidget::setScaleIndependantSize(QSize size) void BaseWidget::setScaleIndependantWidth(int value) { - this->setScaleIndependantSize(QSize(value, this->scaleIndependantSize_.height())); + this->setScaleIndependantSize( + QSize(value, this->scaleIndependantSize_.height())); } void BaseWidget::setScaleIndependantHeight(int value) { - this->setScaleIndependantSize(QSize(this->scaleIndependantSize_.height(), value)); + this->setScaleIndependantSize( + QSize(this->scaleIndependantSize_.height(), value)); } void BaseWidget::childEvent(QChildEvent *event) @@ -114,7 +116,8 @@ void BaseWidget::childEvent(QChildEvent *event) this->widgets_.push_back(widget); } } else if (event->removed()) { - for (auto it = this->widgets_.begin(); it != this->widgets_.end(); it++) { + for (auto it = this->widgets_.begin(); it != this->widgets_.end(); + it++) { if (*it == event->child()) { this->widgets_.erase(it); break; diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 00a841684..eccde0f97 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -9,7 +9,7 @@ #include "util/WindowsHelper.hpp" #include "widgets/Label.hpp" #include "widgets/TooltipWidget.hpp" -#include "widgets/helper/RippleEffectLabel.hpp" +#include "widgets/helper/EffectLabel.hpp" #include "widgets/helper/Shortcut.hpp" #include @@ -41,7 +41,8 @@ namespace chatterino { BaseWindow::BaseWindow(QWidget *parent, Flags _flags) : BaseWidget(parent, - Qt::Window | ((_flags & TopMost) ? Qt::WindowStaysOnTopHint : Qt::WindowFlags())) + Qt::Window | ((_flags & TopMost) ? Qt::WindowStaysOnTopHint + : Qt::WindowFlags())) , enableCustomFrame_(_flags & EnableCustomFrame) , frameless_(_flags & Frameless) , flags_(_flags) @@ -59,7 +60,8 @@ BaseWindow::BaseWindow(QWidget *parent, Flags _flags) this->updateScale(); - createWindowShortcut(this, "CTRL+0", [] { getApp()->settings->uiScale.setValue(0); }); + createWindowShortcut(this, "CTRL+0", + [] { getApp()->settings->uiScale.setValue(0); }); // QTimer::this->scaleChangedEvent(this->getScale()); } @@ -88,19 +90,20 @@ void BaseWindow::init() this->setLayout(layout); { if (!this->frameless_) { - QHBoxLayout *buttonLayout = this->ui_.titlebarBox = new QHBoxLayout(); + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); buttonLayout->setMargin(0); layout->addLayout(buttonLayout); // title Label *title = new Label("Chatterino"); - QObject::connect(this, &QWidget::windowTitleChanged, - [title](const QString &text) { title->setText(text); }); + QObject::connect( + this, &QWidget::windowTitleChanged, + [title](const QString &text) { title->setText(text); }); - QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred); + QSizePolicy policy(QSizePolicy::Ignored, + QSizePolicy::Preferred); policy.setHorizontalStretch(1); - // title->setBaseSize(0, 0); - // title->setScaledContents(true); title->setSizePolicy(policy); buttonLayout->addWidget(title); this->ui_.titleLabel = title; @@ -113,14 +116,18 @@ void BaseWindow::init() TitleBarButton *_exitButton = new TitleBarButton; _exitButton->setButtonStyle(TitleBarButton::Close); - QObject::connect(_minButton, &TitleBarButton::clicked, this, [this] { - this->setWindowState(Qt::WindowMinimized | this->windowState()); - }); - QObject::connect(_maxButton, &TitleBarButton::clicked, this, [this] { - this->setWindowState(this->windowState() == Qt::WindowMaximized - ? Qt::WindowActive - : Qt::WindowMaximized); - }); + QObject::connect(_minButton, &TitleBarButton::clicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect( + _maxButton, &TitleBarButton::clicked, this, [this] { + this->setWindowState(this->windowState() == + Qt::WindowMaximized + ? Qt::WindowActive + : Qt::WindowMaximized); + }); QObject::connect(_exitButton, &TitleBarButton::clicked, this, [this] { this->close(); }); @@ -157,8 +164,10 @@ void BaseWindow::init() QTimer::singleShot(1, this, [this] { getApp()->settings->windowTopMost.connect( [this](bool topMost, auto) { - ::SetWindowPos(HWND(this->winId()), topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, - 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + ::SetWindowPos(HWND(this->winId()), + topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, + 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); }, this->managedConnections_); }); @@ -222,12 +231,13 @@ void BaseWindow::themeChangedEvent() if (this->ui_.titleLabel) { QPalette palette_title; - palette_title.setColor(QPalette::Foreground, - this->theme->isLightTheme() ? "#333" : "#ccc"); + palette_title.setColor( + QPalette::Foreground, + this->theme->isLightTheme() ? "#333" : "#ccc"); this->ui_.titleLabel->setPalette(palette_title); } - for (RippleEffectButton *button : this->ui_.buttons) { + for (Button *button : this->ui_.buttons) { button->setMouseEffectColor(this->theme->window.text); } } else { @@ -240,7 +250,8 @@ void BaseWindow::themeChangedEvent() bool BaseWindow::event(QEvent *event) { - if (event->type() == QEvent::WindowDeactivate /*|| event->type() == QEvent::FocusOut*/) { + if (event->type() == + QEvent::WindowDeactivate /*|| event->type() == QEvent::FocusOut*/) { this->onFocusLost(); } @@ -255,11 +266,11 @@ void BaseWindow::wheelEvent(QWheelEvent *event) if (event->modifiers() & Qt::ControlModifier) { if (event->delta() > 0) { - getApp()->settings->uiScale.setValue( - WindowManager::clampUiScale(getApp()->settings->uiScale.getValue() + 1)); + getApp()->settings->uiScale.setValue(WindowManager::clampUiScale( + getApp()->settings->uiScale.getValue() + 1)); } else { - getApp()->settings->uiScale.setValue( - WindowManager::clampUiScale(getApp()->settings->uiScale.getValue() - 1)); + getApp()->settings->uiScale.setValue(WindowManager::clampUiScale( + getApp()->settings->uiScale.getValue() - 1)); } } } @@ -288,7 +299,8 @@ void BaseWindow::mousePressEvent(QMouseEvent *event) #ifndef Q_OS_WIN if (this->flags_ & FramelessDraggable) { this->movingRelativePos = event->localPos(); - if (auto widget = this->childAt(event->localPos().x(), event->localPos().y())) { + if (auto widget = + this->childAt(event->localPos().x(), event->localPos().y())) { std::function recursiveCheckMouseTracking; recursiveCheckMouseTracking = [&](QWidget *widget) { if (widget == nullptr) { @@ -341,8 +353,8 @@ void BaseWindow::mouseMoveEvent(QMouseEvent *event) BaseWidget::mouseMoveEvent(event); } -TitleBarButton *BaseWindow::addTitleBarButton(const TitleBarButton::Style &style, - std::function onClicked) +TitleBarButton *BaseWindow::addTitleBarButton( + const TitleBarButton::Style &style, std::function onClicked) { TitleBarButton *button = new TitleBarButton; button->setScaleIndependantSize(30, 30); @@ -351,20 +363,22 @@ TitleBarButton *BaseWindow::addTitleBarButton(const TitleBarButton::Style &style this->ui_.titlebarBox->insertWidget(1, button); button->setButtonStyle(style); - QObject::connect(button, &TitleBarButton::clicked, this, [onClicked] { onClicked(); }); + QObject::connect(button, &TitleBarButton::clicked, this, + [onClicked] { onClicked(); }); return button; } -RippleEffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) +EffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) { - RippleEffectLabel *button = new RippleEffectLabel; + EffectLabel *button = new EffectLabel; button->setScaleIndependantHeight(30); this->ui_.buttons.push_back(button); this->ui_.titlebarBox->insertWidget(1, button); - QObject::connect(button, &RippleEffectLabel::clicked, this, [onClicked] { onClicked(); }); + QObject::connect(button, &EffectLabel::clicked, this, + [onClicked] { onClicked(); }); return button; } @@ -375,7 +389,8 @@ void BaseWindow::changeEvent(QEvent *) #ifdef USEWINSDK if (this->ui_.maxButton) { - this->ui_.maxButton->setButtonStyle(this->windowState() & Qt::WindowMaximized + this->ui_.maxButton->setButtonStyle(this->windowState() & + Qt::WindowMaximized ? TitleBarButton::Unmaximize : TitleBarButton::Maximize); } @@ -416,8 +431,7 @@ void BaseWindow::closeEvent(QCloseEvent *) void BaseWindow::moveIntoDesktopRect(QWidget *parent) { - if (!this->stayInScreenRect_) - return; + if (!this->stayInScreenRect_) return; // move the widget into the screen geometry if it's not already in there QDesktopWidget *desktop = QApplication::desktop(); @@ -438,14 +452,18 @@ void BaseWindow::moveIntoDesktopRect(QWidget *parent) p.setY(s.bottom() - this->height()); } - if (p != this->pos()) - this->move(p); + if (p != this->pos()) this->move(p); } -bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, long *result) +bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, + long *result) { #ifdef USEWINSDK +#if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1)) + MSG *msg = *reinterpret_cast(message); +#else MSG *msg = reinterpret_cast(message); +#endif bool returnValue = false; @@ -503,8 +521,10 @@ void BaseWindow::paintEvent(QPaintEvent *) void BaseWindow::updateScale() { - auto scale = this->nativeScale_ * - (this->flags_ & DisableCustomScaling ? 1 : getApp()->windows->getUiScaleValue()); + auto scale = + this->nativeScale_ * (this->flags_ & DisableCustomScaling + ? 1 + : getApp()->windows->getUiScaleValue()); this->setScale(scale); for (auto child : this->findChildren()) { @@ -541,9 +561,11 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter) if (this->hasCustomWindowFrame()) { QPainter painter(this); - QColor bg = this->overrideBackgroundColor_.value_or(this->theme->window.background); + QColor bg = this->overrideBackgroundColor_.value_or( + this->theme->window.background); - painter.fillRect(QRect(0, 1, this->width() - 0, this->height() - 0), bg); + painter.fillRect(QRect(0, 1, this->width() - 0, this->height() - 0), + bg); } #endif } @@ -561,7 +583,8 @@ bool BaseWindow::handleDPICHANGED(MSG *msg) auto *prcNewWindow = reinterpret_cast(msg->lParam); SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, prcNewWindow->right - prcNewWindow->left, - prcNewWindow->bottom - prcNewWindow->top, SWP_NOZORDER | SWP_NOACTIVATE); + prcNewWindow->bottom - prcNewWindow->top, + SWP_NOZORDER | SWP_NOACTIVATE); } firstResize = false; @@ -605,7 +628,8 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) // int cy = GetSystemMetrics(SM_CYSIZEFRAME); if (msg->wParam == TRUE) { - NCCALCSIZE_PARAMS *ncp = (reinterpret_cast(msg->lParam)); + NCCALCSIZE_PARAMS *ncp = + (reinterpret_cast(msg->lParam)); ncp->lppos->flags |= SWP_NOREDRAW; RECT *clientRect = &ncp->rgrc[0]; @@ -634,7 +658,8 @@ bool BaseWindow::handleSIZE(MSG *msg) if (msg->wParam == SIZE_MAXIMIZED) { auto offset = int(this->getScale() * 8); - this->ui_.windowLayout->setContentsMargins(offset, offset, offset, offset); + this->ui_.windowLayout->setContentsMargins(offset, offset, + offset, offset); } else { this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); } @@ -686,23 +711,23 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) } if (resizeWidth && resizeHeight) { // bottom left corner - if (x >= winrect.left && x < winrect.left + border_width && y < winrect.bottom && - y >= winrect.bottom - border_width) { + if (x >= winrect.left && x < winrect.left + border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) { *result = HTBOTTOMLEFT; } // bottom right corner - if (x < winrect.right && x >= winrect.right - border_width && y < winrect.bottom && - y >= winrect.bottom - border_width) { + if (x < winrect.right && x >= winrect.right - border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) { *result = HTBOTTOMRIGHT; } // top left corner - if (x >= winrect.left && x < winrect.left + border_width && y >= winrect.top && - y < winrect.top + border_width) { + if (x >= winrect.left && x < winrect.left + border_width && + y >= winrect.top && y < winrect.top + border_width) { *result = HTTOPLEFT; } // top right corner - if (x < winrect.right && x >= winrect.right - border_width && y >= winrect.top && - y < winrect.top + border_width) { + if (x < winrect.right && x >= winrect.right - border_width && + y >= winrect.top && y < winrect.top + border_width) { *result = HTTOPRIGHT; } } diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index d25a1be2e..eac2ee55e 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -12,8 +12,8 @@ typedef struct tagMSG MSG; namespace chatterino { -class RippleEffectButton; -class RippleEffectLabel; +class Button; +class EffectLabel; class TitleBarButton; class BaseWindow : public BaseWidget @@ -38,7 +38,7 @@ public: bool hasCustomWindowFrame(); TitleBarButton *addTitleBarButton(const TitleBarButton::Style &style, std::function onClicked); - RippleEffectLabel *addTitleBarLabel(std::function onClicked); + EffectLabel *addTitleBarLabel(std::function onClicked); void setStayInScreenRect(bool value); bool getStayInScreenRect() const; @@ -55,7 +55,8 @@ public: pajlada::Signals::NoArgSignal closing; protected: - virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, + long *result) override; virtual void scaleChangedEvent(float) override; virtual void paintEvent(QPaintEvent *) override; @@ -108,7 +109,7 @@ private: TitleBarButton *maxButton = nullptr; TitleBarButton *exitButton = nullptr; QWidget *layoutBase = nullptr; - std::vector buttons; + std::vector