diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..9880f2ce1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,3 @@ +# Description + + diff --git a/.gitmodules b/.gitmodules index 45c84aee1..da905b54d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,4 +17,4 @@ [submodule "lib/appbase"] path = lib/appbase - url = https://github.com/fourtf/appbase + url = https://github.com/Chatterino/appbase diff --git a/.travis.yml b/.travis.yml index 1267bf14c..1ad6dc6e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,35 @@ -before_install: - - sudo add-apt-repository --yes ppa:ubuntu-sdk-team/ppa - - sudo apt-get update -qq - - sudo apt-get install qtbase5-dev qtdeclarative5-dev libqt5webkit5-dev libsqlite3-dev - - sudo apt-get install qt5-default qttools5-dev-tools +os: osx +osx_image: xcode10.2 + +addons: + homebrew: + packages: + - boost + - openssl + - rapidjson + - qt + - p7zip + +compiler: clang script: - - qmake -qt=qt5 -v - - qmake -qt=qt5 - - make + - mkdir build && cd build + - /usr/local/opt/qt/bin/qmake .. && make -j8 + - /usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg + - mv chatterino.dmg chatterino-osx.dmg + +before_deploy: + - git config --global user.email "builds@travis-ci.com" + - git config --global user.name "Travis CI" + - export GIT_TAG=nightly-build + - git tag $GIT_TAG -f + +deploy: + skip_cleanup: true + provider: releases + api_key: + secure: ZzS55wlwtLAVEBaDDMqiuqZwuTpvLbNnaNw0enfiqpjWT7hgbbp/SBw2rbYIkVqm7tBHCLnEzKto6p4Gz6ROo0gGACARmx7EwIloX18rMCuBWygNHRyVruDSlmEOLWRqYByDbUdCkKhYr9aegnkm7zhzCmSBCTW28/uVlxM2bTHIgqKEpB4k1W8OqKdJDxqZKeF4r7nDNSOx5ylhpiK+WNFK8yfiaF1SQlSwsdv9o1RkbJlew7iigvHvEM2kDMkiMWYlJ2khkUWVCVQDQGe4/ya5pgTIHDLu5sZuclp5zhgfDf1U3STvsbQWvxJfsmCId7IQHJ83OSFeoUf6y849i3GMqlNi3aXrxEx0fi0dILQ76/Sj246FPMA4kC0/W49uaxqD784wFuJDjSWeWwi/NPoJ/gz0mGZy+08BoztOGqqOKjJJdESBYTio71N8VcK09zQ0LjXRmX+g3BbrK6a2F3hiMKeuYwdaN2/KdMMoqFDau6L3fXLdpcHKdJC8K/yzJtyyIe0CRB2nj8sZLHfxDwoRm7gOTDXq1zPL7CP9cCwCnCR6nm3CqUW/CnSWuMKpSoQRlP5EBI7zzYT2/tZc/vat5nob7Xif6yFF9fh/VHx4tC6zsfkA1nPPN3+QpdVInRo7dCVxtTqey5FdVjSiv7n11TrFhZ7+Fr5x6CZqa58= + file: "chatterino-osx.dmg" + prerelease: true + on: + branch: nightly diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index f594e2c10..0c717d8b6 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -4,7 +4,7 @@ Note on Qt version compatibility: If you are installing Qt from a package manage ## Ubuntu 18.04 *most likely works the same for other Debian-like distros* -1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev` +1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev` 1. Open `chatterino.pro` with QT Creator and build ## Arch Linux diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md index 7aedb0cab..40df39bb0 100644 --- a/BUILDING_ON_WINDOWS.md +++ b/BUILDING_ON_WINDOWS.md @@ -102,7 +102,14 @@ To produce all supplement files for a standalone build, follow these steps (adju cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_11_2_MSVC2017_64bit-Release\release C:\Qt\5.11.2\msvc2017_64\bin\windeployqt.exe chatterino.exe + + 5. Go to `C:\local\bin\` and copy these dll's into your `release folder`. + + libssl-1_1-x64.dll + libcrypto-1_1-x64.dll + ssleay32.dll + libeay32.dll - 5. The `releases` directory will now be populated with all the required files to make the chatterino build standalone. + 6. The `releases` directory will now be populated with all the required files to make the chatterino build standalone. You can now create a zip archive of all the contents in `releases` and distribute the program as is, without requiring any development tools to be present on the target system. (However, the vcredist package must be present, as usual - see the [README](README.md)). diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..4bcd983a4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.8) + +project(chatterino) + +include_directories(src) + +set(chatterino_SOURCES + src/common/UsernameSet.cpp + ) + +find_package(Qt5Widgets CONFIG REQUIRED) +find_package(Qt5 5.9.0 REQUIRED COMPONENTS + Core + ) + +# set(CMAKE_AUTOMOC ON) + +if (BUILD_TESTS) + message("++ Tests enabled") + find_package(GTest) + enable_testing() + + add_executable(chatterino-test + ${chatterino_SOURCES} + + tests/src/main.cpp + tests/src/UsernameSet.cpp + ) + + target_link_libraries(chatterino-test Qt5::Core) + + target_link_libraries(chatterino-test gtest gtest_main) + + gtest_discover_tests(chatterino-test) +else() + message(FATAL_ERROR "This cmake file is only intended for tests right now. Use qmake to build chatterino2") +endif() diff --git a/README.md b/README.md index d35238e83..defff3340 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,4 @@ The code is formated using clang format in Qt Creator. [.clang-format](https://g 7. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None` Qt creator should now format the documents when saving it. + diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..a589baed7 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,65 @@ +version: 1.0.{build} +branches: + only: + - nightly +image: Visual Studio 2017 +platform: Any CPU +clone_depth: 1 +init: +- cmd: '' +install: +- cmd: >- + git submodule update --init --recursive + + set QTDIR=C:\Qt\5.11\msvc2017_64 + + set PATH=%PATH%;%QTDIR%\bin + + call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 +build_script: +- cmd: >- + curl -fsS -o openssl.7z https://pajlada.se/files/openssl.7z + + 7z x openssl.7z + + dir + + mkdir build + + cd build + + qmake ../chatterino.pro BOOST_DIRECTORY="C:\Libraries\boost_1_64_0" BOOST_LIB_SUFFIX="lib64-msvc-14.1" OPENSSL_DIRECTORY="%APPVEYOR_BUILD_FOLDER%\openssl" DEFINES+="CHATTERINO_NIGHTLY_VERSION_STRING=\\\"$$system(git describe --always)-$$system(git rev-list master --count)\\\"" + + set cl=/MP + + nmake /S /NOLOGO + + git clone https://github.com/pajlada/chatterino2-dlls.git + + mkdir Chatterino2 + + cp ../openssl/bin/libcrypto*.dll ../openssl/bin/libssl*.dll Chatterino2/ + + cp chatterino2-dlls/*.dll Chatterino2/ + + windeployqt release/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ + + cp release/chatterino.exe Chatterino2/ + + 7z a chatterino-windows-x86-64.zip Chatterino2/ +artifacts: +- path: build/chatterino-windows-x86-64.zip + name: chatterino +deploy: +- provider: GitHub + tag: nightly-build + release: nightly-build + description: 'nightly v$(appveyor_build_version) built $(APPVEYOR_REPO_COMMIT_TIMESTAMP)\nLast change: $(APPVEYOR_REPO_COMMIT_MESSAGE) \n$(APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED)' + auth_token: + secure: sAJzAbiQSsYZLT+byDar9u61X0E9o35anaPMSFkOzdHeDFHjx1kW4cDP/4EEbxhx + repository: Chatterino/chatterino2 + artifact: build/chatterino-windows-x86-64.zip + prerelease: true + force_update: true + on: + branch: nightly diff --git a/chatterino.pro b/chatterino.pro index dafca10d3..56517ef3c 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -1,403 +1,424 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2016-12-28T18:23:35 -# -#------------------------------------------------- - -message(----) - -# define project shit -QT += widgets core gui network multimedia svg concurrent -CONFIG += communi -COMMUNI += core model util - -INCLUDEPATH += src/ -TARGET = chatterino -TEMPLATE = app -PRECOMPILED_HEADER = src/PrecompiledHeader.hpp -CONFIG += precompile_header -DEFINES += CHATTERINO -DEFINES += "AB_NAMESPACE=chatterino" -DEFINES += AB_CUSTOM_THEME -DEFINES += AB_CUSTOM_SETTINGS -CONFIG += AB_NOT_STANDALONE - -useBreakpad { - LIBS += -L$$PWD/lib/qBreakpad/handler/build - include(lib/qBreakpad/qBreakpad.pri) - DEFINES += C_USE_BREAKPAD -} - -# https://bugreports.qt.io/browse/QTBUG-27018 -equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { - TARGET = bin/chatterino -} - -# Icons -#macx:ICON = resources/images/chatterino2.icns -win32:RC_FILE = resources/windows.rc - -macx { - LIBS += -L/usr/local/lib -} - -# Submodules -include(lib/appbase.pri) -include(lib/humanize.pri) -DEFINES += IRC_NAMESPACE=Communi -include(lib/libcommuni.pri) -include(lib/websocketpp.pri) -include(lib/openssl.pri) -include(lib/wintoast.pri) - -# Optional feature: QtWebEngine -#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { -# message(Using QWebEngine) -# QT += webenginewidgets -# DEFINES += "USEWEBENGINE" -#} - -SOURCES += \ - src/Application.cpp \ - src/common/Channel.cpp \ - src/common/CompletionModel.cpp \ - src/common/NetworkData.cpp \ - src/common/NetworkManager.cpp \ - src/common/NetworkRequest.cpp \ - src/common/NetworkResult.cpp \ - src/common/NetworkTimer.cpp \ - src/controllers/accounts/Account.cpp \ - src/controllers/accounts/AccountController.cpp \ - src/controllers/accounts/AccountModel.cpp \ - src/controllers/commands/Command.cpp \ - src/controllers/commands/CommandController.cpp \ - src/controllers/commands/CommandModel.cpp \ - src/controllers/highlights/HighlightController.cpp \ - src/controllers/highlights/HighlightModel.cpp \ - src/controllers/highlights/HighlightBlacklistModel.cpp \ - src/controllers/highlights/UserHighlightModel.cpp \ - src/controllers/ignores/IgnoreController.cpp \ - src/controllers/ignores/IgnoreModel.cpp \ - src/controllers/notifications/NotificationController.cpp \ - src/controllers/taggedusers/TaggedUser.cpp \ - src/controllers/taggedusers/TaggedUsersController.cpp \ - src/controllers/taggedusers/TaggedUsersModel.cpp \ - src/main.cpp \ - src/messages/Image.cpp \ - src/messages/layouts/MessageLayout.cpp \ - src/messages/layouts/MessageLayoutContainer.cpp \ - src/messages/layouts/MessageLayoutElement.cpp \ - src/messages/Link.cpp \ - src/messages/Message.cpp \ - src/messages/MessageBuilder.cpp \ - src/messages/MessageColor.cpp \ - src/messages/MessageElement.cpp \ - src/providers/emoji/Emojis.cpp \ - src/providers/irc/AbstractIrcServer.cpp \ - src/providers/irc/IrcAccount.cpp \ - src/providers/irc/IrcChannel2.cpp \ - src/providers/irc/IrcConnection2.cpp \ - src/providers/irc/IrcServer.cpp \ - src/providers/twitch/IrcMessageHandler.cpp \ - src/providers/twitch/PartialTwitchUser.cpp \ - src/providers/twitch/PubsubActions.cpp \ - src/providers/twitch/PubsubHelpers.cpp \ - src/providers/twitch/TwitchAccount.cpp \ - src/providers/twitch/TwitchAccountManager.cpp \ - src/providers/twitch/TwitchChannel.cpp \ - src/providers/twitch/TwitchEmotes.cpp \ - src/providers/twitch/TwitchHelpers.cpp \ - src/providers/twitch/TwitchMessageBuilder.cpp \ - src/providers/twitch/TwitchServer.cpp \ - src/providers/twitch/TwitchUser.cpp \ - src/singletons/helper/GifTimer.cpp \ - src/singletons/helper/LoggingChannel.cpp \ - src/controllers/moderationactions/ModerationAction.cpp \ - src/singletons/WindowManager.cpp \ - src/util/DebugCount.cpp \ - src/util/RapidjsonHelpers.cpp \ - src/util/StreamLink.cpp \ - src/widgets/AccountSwitchPopupWidget.cpp \ - src/widgets/AccountSwitchWidget.cpp \ - src/widgets/AttachedWindow.cpp \ - src/widgets/dialogs/EmotePopup.cpp \ - src/widgets/dialogs/LastRunCrashDialog.cpp \ - src/widgets/dialogs/LoginDialog.cpp \ - src/widgets/dialogs/LogsPopup.cpp \ - src/widgets/dialogs/NotificationPopup.cpp \ - src/widgets/dialogs/QualityPopup.cpp \ - src/widgets/dialogs/SelectChannelDialog.cpp \ - src/widgets/dialogs/SettingsDialog.cpp \ - src/widgets/dialogs/TextInputDialog.cpp \ - src/widgets/dialogs/UserInfoPopup.cpp \ - src/widgets/dialogs/WelcomeDialog.cpp \ - src/widgets/helper/ChannelView.cpp \ - src/widgets/helper/ComboBoxItemDelegate.cpp \ - src/widgets/helper/DebugPopup.cpp \ - src/widgets/helper/EditableModelView.cpp \ - src/widgets/helper/NotebookButton.cpp \ - src/widgets/helper/NotebookTab.cpp \ - src/widgets/helper/ResizingTextEdit.cpp \ - src/widgets/helper/ScrollbarHighlight.cpp \ - src/widgets/helper/SearchPopup.cpp \ - src/widgets/helper/SettingsDialogTab.cpp \ - src/widgets/Notebook.cpp \ - src/widgets/Scrollbar.cpp \ - src/widgets/settingspages/AboutPage.cpp \ - src/widgets/settingspages/AccountsPage.cpp \ - src/widgets/settingspages/BrowserExtensionPage.cpp \ - src/widgets/settingspages/CommandPage.cpp \ - src/widgets/settingspages/EmotesPage.cpp \ - src/widgets/settingspages/ExternalToolsPage.cpp \ - src/widgets/settingspages/HighlightingPage.cpp \ - src/widgets/settingspages/KeyboardSettingsPage.cpp \ - src/widgets/settingspages/LogsPage.cpp \ - src/widgets/settingspages/ModerationPage.cpp \ - src/widgets/settingspages/NotificationPage.cpp \ - src/widgets/settingspages/SettingsPage.cpp \ - src/widgets/settingspages/SpecialChannelsPage.cpp \ - src/widgets/splits/Split.cpp \ - src/widgets/splits/SplitContainer.cpp \ - src/widgets/splits/SplitHeader.cpp \ - src/widgets/splits/SplitInput.cpp \ - src/widgets/splits/SplitOverlay.cpp \ - src/widgets/StreamView.cpp \ - src/widgets/Window.cpp \ - src/common/LinkParser.cpp \ - src/controllers/moderationactions/ModerationActions.cpp \ - src/singletons/NativeMessaging.cpp \ - src/singletons/Emotes.cpp \ - src/singletons/Logging.cpp \ - src/singletons/Paths.cpp \ - src/singletons/Resources.cpp \ - src/singletons/Settings.cpp \ - src/singletons/Updates.cpp \ - src/singletons/Theme.cpp \ - src/controllers/moderationactions/ModerationActionModel.cpp \ - src/widgets/settingspages/LookPage.cpp \ - src/widgets/settingspages/FeelPage.cpp \ - src/util/InitUpdateButton.cpp \ - src/widgets/dialogs/UpdateDialog.cpp \ - src/widgets/settingspages/IgnoresPage.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/LinkResolver.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/controllers/notifications/NotificationModel.cpp \ - src/singletons/Toasts.cpp \ - src/common/DownloadManager.cpp \ - src/messages/MessageContainer.cpp \ - src/common/UsernameSet.cpp \ - src/widgets/settingspages/AdvancedPage.cpp \ - src/util/IncognitoBrowser.cpp \ - src/widgets/splits/ClosedSplits.cpp \ - src/providers/ffz/FfzModBadge.cpp \ - src/widgets/settingspages/GeneralPage.cpp \ - src/providers/twitch/ChatroomChannel.cpp - -HEADERS += \ - src/Application.hpp \ - src/common/Channel.hpp \ - src/common/Common.hpp \ - src/common/CompletionModel.hpp \ - src/common/Atomic.hpp \ - src/common/NetworkCommon.hpp \ - src/common/NetworkData.hpp \ - src/common/NetworkManager.hpp \ - src/common/NetworkRequest.hpp \ - src/common/NetworkRequester.hpp \ - src/common/NetworkResult.hpp \ - src/common/NetworkTimer.hpp \ - src/common/NetworkWorker.hpp \ - src/common/NullablePtr.hpp \ - src/common/ProviderId.hpp \ - src/common/SignalVectorModel.hpp \ - src/common/Version.hpp \ - src/controllers/accounts/Account.hpp \ - src/controllers/accounts/AccountController.hpp \ - src/controllers/accounts/AccountModel.hpp \ - src/controllers/commands/Command.hpp \ - src/controllers/commands/CommandController.hpp \ - src/controllers/commands/CommandModel.hpp \ - src/controllers/highlights/HighlightController.hpp \ - src/controllers/highlights/HighlightModel.hpp \ - src/controllers/highlights/HighlightBlacklistModel.hpp \ - src/controllers/highlights/HighlightPhrase.hpp \ - src/controllers/highlights/HighlightBlacklistUser.hpp \ - src/controllers/highlights/UserHighlightModel.hpp \ - src/controllers/ignores/IgnoreController.hpp \ - src/controllers/ignores/IgnoreModel.hpp \ - src/controllers/ignores/IgnorePhrase.hpp \ - src/controllers/notifications/NotificationController.hpp \ - src/controllers/taggedusers/TaggedUser.hpp \ - src/controllers/taggedusers/TaggedUsersController.hpp \ - src/controllers/taggedusers/TaggedUsersModel.hpp \ - src/messages/Image.hpp \ - src/messages/layouts/MessageLayout.hpp \ - src/messages/layouts/MessageLayoutContainer.hpp \ - src/messages/layouts/MessageLayoutElement.hpp \ - src/messages/LimitedQueue.hpp \ - src/messages/LimitedQueueSnapshot.hpp \ - src/messages/Link.hpp \ - src/messages/Message.hpp \ - src/messages/MessageBuilder.hpp \ - src/messages/MessageColor.hpp \ - src/messages/MessageElement.hpp \ - src/messages/Selection.hpp \ - src/PrecompiledHeader.hpp \ - src/providers/emoji/Emojis.hpp \ - src/providers/irc/AbstractIrcServer.hpp \ - src/providers/irc/IrcAccount.hpp \ - src/providers/irc/IrcChannel2.hpp \ - src/providers/irc/IrcConnection2.hpp \ - src/providers/irc/IrcServer.hpp \ - src/providers/twitch/EmoteValue.hpp \ - src/providers/twitch/IrcMessageHandler.hpp \ - src/providers/twitch/PartialTwitchUser.hpp \ - src/providers/twitch/PubsubActions.hpp \ - src/providers/twitch/PubsubHelpers.hpp \ - src/providers/twitch/TwitchAccount.hpp \ - src/providers/twitch/TwitchAccountManager.hpp \ - src/providers/twitch/TwitchChannel.hpp \ - src/providers/twitch/TwitchEmotes.hpp \ - src/providers/twitch/TwitchHelpers.hpp \ - src/providers/twitch/TwitchMessageBuilder.hpp \ - src/providers/twitch/TwitchServer.hpp \ - src/providers/twitch/TwitchUser.hpp \ - src/singletons/helper/GifTimer.hpp \ - src/singletons/helper/LoggingChannel.hpp \ - src/controllers/moderationactions/ModerationAction.hpp \ - src/singletons/WindowManager.hpp \ - src/util/ConcurrentMap.hpp \ - src/util/DebugCount.hpp \ - src/util/IrcHelpers.hpp \ - src/util/LayoutCreator.hpp \ - src/util/QStringHash.hpp \ - src/util/RapidjsonHelpers.hpp \ - src/util/RemoveScrollAreaBackground.hpp \ - src/util/SharedPtrElementLess.hpp \ - src/util/StandardItemHelper.hpp \ - src/util/StreamLink.hpp \ - src/widgets/AccountSwitchPopupWidget.hpp \ - src/widgets/AccountSwitchWidget.hpp \ - src/widgets/AttachedWindow.hpp \ - src/widgets/dialogs/EmotePopup.hpp \ - src/widgets/dialogs/LastRunCrashDialog.hpp \ - src/widgets/dialogs/LoginDialog.hpp \ - src/widgets/dialogs/LogsPopup.hpp \ - src/widgets/dialogs/NotificationPopup.hpp \ - src/widgets/dialogs/QualityPopup.hpp \ - src/widgets/dialogs/SelectChannelDialog.hpp \ - src/widgets/dialogs/SettingsDialog.hpp \ - src/widgets/dialogs/TextInputDialog.hpp \ - src/widgets/dialogs/UserInfoPopup.hpp \ - src/widgets/dialogs/WelcomeDialog.hpp \ - src/widgets/helper/ChannelView.hpp \ - src/widgets/helper/ComboBoxItemDelegate.hpp \ - src/widgets/helper/DebugPopup.hpp \ - src/widgets/helper/EditableModelView.hpp \ - src/widgets/helper/Line.hpp \ - src/widgets/helper/NotebookButton.hpp \ - src/widgets/helper/NotebookTab.hpp \ - src/widgets/helper/ResizingTextEdit.hpp \ - src/widgets/helper/ScrollbarHighlight.hpp \ - src/widgets/helper/SearchPopup.hpp \ - src/widgets/helper/SettingsDialogTab.hpp \ - src/widgets/Notebook.hpp \ - src/widgets/Scrollbar.hpp \ - src/widgets/settingspages/AboutPage.hpp \ - src/widgets/settingspages/AccountsPage.hpp \ - src/widgets/settingspages/BrowserExtensionPage.hpp \ - src/widgets/settingspages/CommandPage.hpp \ - src/widgets/settingspages/EmotesPage.hpp \ - src/widgets/settingspages/ExternalToolsPage.hpp \ - src/widgets/settingspages/HighlightingPage.hpp \ - src/widgets/settingspages/KeyboardSettingsPage.hpp \ - src/widgets/settingspages/LogsPage.hpp \ - src/widgets/settingspages/ModerationPage.hpp \ - src/widgets/settingspages/NotificationPage.hpp \ - src/widgets/settingspages/SettingsPage.hpp \ - src/widgets/settingspages/SpecialChannelsPage.hpp \ - src/widgets/splits/Split.hpp \ - src/widgets/splits/SplitContainer.hpp \ - src/widgets/splits/SplitHeader.hpp \ - src/widgets/splits/SplitInput.hpp \ - src/widgets/splits/SplitOverlay.hpp \ - src/widgets/StreamView.hpp \ - src/widgets/Window.hpp \ - src/providers/twitch/TwitchCommon.hpp \ - src/util/IsBigEndian.hpp \ - src/common/LinkParser.hpp \ - src/controllers/moderationactions/ModerationActions.hpp \ - src/singletons/Emotes.hpp \ - src/singletons/Logging.hpp \ - src/singletons/Paths.hpp \ - src/singletons/Resources.hpp \ - src/singletons/Settings.hpp \ - src/singletons/Updates.hpp \ - src/singletons/NativeMessaging.hpp \ - src/singletons/Theme.hpp \ - src/common/SignalVector.hpp \ - src/widgets/dialogs/LogsPopup.hpp \ - src/controllers/moderationactions/ModerationActionModel.hpp \ - src/widgets/settingspages/LookPage.hpp \ - src/widgets/settingspages/FeelPage.hpp \ - src/util/InitUpdateButton.hpp \ - src/widgets/dialogs/UpdateDialog.hpp \ - src/widgets/settingspages/IgnoresPage.hpp \ - src/providers/twitch/PubsubClient.hpp \ - src/providers/twitch/TwitchApi.hpp \ - src/messages/Emote.hpp \ - src/messages/ImageSet.hpp \ - src/providers/bttv/BttvEmotes.hpp \ - src/providers/LinkResolver.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/controllers/notifications/NotificationModel.hpp \ - src/singletons/Toasts.hpp \ - src/common/DownloadManager.hpp \ - src/messages/MessageContainer.hpp \ - src/common/UsernameSet.hpp \ - src/widgets/settingspages/AdvancedPage.hpp \ - src/util/IncognitoBrowser.hpp \ - src/widgets/splits/ClosedSplits.hpp \ - src/providers/ffz/FfzModBadge.hpp \ - src/widgets/settingspages/GeneralPage.hpp \ - src/messages/HistoricMessageAppearance.hpp \ - src/providers/twitch/ChatroomChannel.hpp - -RESOURCES += \ - resources/resources.qrc \ - resources/resources_autogenerated.qrc - -DISTFILES += - -FORMS += - -# do not use windows min/max macros -#win32 { -# DEFINES += NOMINMAX -#} +QT += widgets core gui network multimedia svg concurrent +CONFIG += communi +COMMUNI += core model util + +INCLUDEPATH += src/ +TARGET = chatterino +TEMPLATE = app +PRECOMPILED_HEADER = src/PrecompiledHeader.hpp +CONFIG += precompile_header +DEFINES += CHATTERINO +DEFINES += "AB_NAMESPACE=chatterino" +DEFINES += AB_CUSTOM_THEME +DEFINES += AB_CUSTOM_SETTINGS +CONFIG += AB_NOT_STANDALONE + +useBreakpad { + LIBS += -L$$PWD/lib/qBreakpad/handler/build + include(lib/qBreakpad/qBreakpad.pri) + DEFINES += C_USE_BREAKPAD +} + +# https://bugreports.qt.io/browse/QTBUG-27018 +equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { + TARGET = bin/chatterino +} + +# Icons +macx:ICON = resources/chatterino.icns +win32:RC_FILE = resources/windows.rc + +macx { + LIBS += -L/usr/local/lib +} + +# Submodules +include(lib/appbase.pri) +include(lib/humanize.pri) +DEFINES += IRC_NAMESPACE=Communi +include(lib/libcommuni.pri) +include(lib/websocketpp.pri) +include(lib/openssl.pri) +include(lib/wintoast.pri) + +# Optional feature: QtWebEngine +#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { +# message(Using QWebEngine) +# QT += webenginewidgets +# DEFINES += "USEWEBENGINE" +#} + +SOURCES += \ + src/Application.cpp \ + src/autogenerated/ResourcesAutogen.cpp \ + src/BrowserExtension.cpp \ + src/common/Channel.cpp \ + src/common/CompletionModel.cpp \ + src/common/DownloadManager.cpp \ + src/common/Env.cpp \ + src/common/LinkParser.cpp \ + src/common/NetworkData.cpp \ + src/common/NetworkManager.cpp \ + src/common/NetworkRequest.cpp \ + src/common/NetworkResult.cpp \ + src/common/NetworkTimer.cpp \ + src/common/UsernameSet.cpp \ + src/controllers/accounts/Account.cpp \ + src/controllers/accounts/AccountController.cpp \ + src/controllers/accounts/AccountModel.cpp \ + src/controllers/commands/Command.cpp \ + src/controllers/commands/CommandController.cpp \ + src/controllers/commands/CommandModel.cpp \ + src/controllers/highlights/HighlightBlacklistModel.cpp \ + src/controllers/highlights/HighlightController.cpp \ + src/controllers/highlights/HighlightModel.cpp \ + src/controllers/highlights/UserHighlightModel.cpp \ + src/controllers/ignores/IgnoreController.cpp \ + src/controllers/ignores/IgnoreModel.cpp \ + src/controllers/moderationactions/ModerationAction.cpp \ + src/controllers/moderationactions/ModerationActionModel.cpp \ + src/controllers/moderationactions/ModerationActions.cpp \ + src/controllers/notifications/NotificationController.cpp \ + src/controllers/notifications/NotificationModel.cpp \ + src/controllers/taggedusers/TaggedUser.cpp \ + src/controllers/taggedusers/TaggedUsersController.cpp \ + src/controllers/taggedusers/TaggedUsersModel.cpp \ + src/main.cpp \ + src/messages/Emote.cpp \ + src/messages/Image.cpp \ + src/messages/ImageSet.cpp \ + src/messages/layouts/MessageLayout.cpp \ + src/messages/layouts/MessageLayoutContainer.cpp \ + src/messages/layouts/MessageLayoutElement.cpp \ + src/messages/Link.cpp \ + src/messages/Message.cpp \ + src/messages/MessageBuilder.cpp \ + src/messages/MessageColor.cpp \ + src/messages/MessageContainer.cpp \ + src/messages/MessageElement.cpp \ + src/providers/bttv/BttvEmotes.cpp \ + src/providers/bttv/LoadBttvChannelEmote.cpp \ + src/providers/chatterino/ChatterinoBadges.cpp \ + src/providers/emoji/Emojis.cpp \ + src/providers/ffz/FfzEmotes.cpp \ + src/providers/ffz/FfzModBadge.cpp \ + src/providers/irc/AbstractIrcServer.cpp \ + src/providers/irc/IrcAccount.cpp \ + src/providers/irc/IrcChannel2.cpp \ + src/providers/irc/IrcConnection2.cpp \ + src/providers/irc/IrcServer.cpp \ + src/providers/LinkResolver.cpp \ + src/providers/twitch/ChatroomChannel.cpp \ + src/providers/twitch/IrcMessageHandler.cpp \ + src/providers/twitch/PartialTwitchUser.cpp \ + src/providers/twitch/PubsubActions.cpp \ + src/providers/twitch/PubsubClient.cpp \ + src/providers/twitch/PubsubHelpers.cpp \ + src/providers/twitch/TwitchAccount.cpp \ + src/providers/twitch/TwitchAccountManager.cpp \ + src/providers/twitch/TwitchApi.cpp \ + src/providers/twitch/TwitchBadges.cpp \ + src/providers/twitch/TwitchChannel.cpp \ + src/providers/twitch/TwitchEmotes.cpp \ + src/providers/twitch/TwitchHelpers.cpp \ + src/providers/twitch/TwitchMessageBuilder.cpp \ + src/providers/twitch/TwitchParseCheerEmotes.cpp \ + src/providers/twitch/TwitchServer.cpp \ + src/providers/twitch/TwitchUser.cpp \ + src/RunGui.cpp \ + src/singletons/Badges.cpp \ + src/singletons/Emotes.cpp \ + src/singletons/helper/GifTimer.cpp \ + src/singletons/helper/LoggingChannel.cpp \ + src/singletons/Logging.cpp \ + src/singletons/NativeMessaging.cpp \ + src/singletons/Paths.cpp \ + src/singletons/Resources.cpp \ + src/singletons/Settings.cpp \ + src/singletons/Theme.cpp \ + src/singletons/Toasts.cpp \ + src/singletons/Updates.cpp \ + src/singletons/WindowManager.cpp \ + src/singletons/TooltipPreviewImage.cpp \ + src/util/DebugCount.cpp \ + src/util/FormatTime.cpp \ + src/util/IncognitoBrowser.cpp \ + src/util/InitUpdateButton.cpp \ + src/util/JsonQuery.cpp \ + src/util/RapidjsonHelpers.cpp \ + src/util/StreamLink.cpp \ + src/widgets/AccountSwitchPopupWidget.cpp \ + src/widgets/AccountSwitchWidget.cpp \ + src/widgets/AttachedWindow.cpp \ + src/widgets/dialogs/EmotePopup.cpp \ + src/widgets/dialogs/LastRunCrashDialog.cpp \ + src/widgets/dialogs/LoginDialog.cpp \ + src/widgets/dialogs/LogsPopup.cpp \ + src/widgets/dialogs/NotificationPopup.cpp \ + src/widgets/dialogs/QualityPopup.cpp \ + src/widgets/dialogs/SelectChannelDialog.cpp \ + src/widgets/dialogs/SettingsDialog.cpp \ + src/widgets/dialogs/TextInputDialog.cpp \ + src/widgets/dialogs/UpdateDialog.cpp \ + src/widgets/dialogs/UserInfoPopup.cpp \ + src/widgets/dialogs/WelcomeDialog.cpp \ + src/widgets/helper/ChannelView.cpp \ + src/widgets/helper/ComboBoxItemDelegate.cpp \ + src/widgets/helper/DebugPopup.cpp \ + src/widgets/helper/EditableModelView.cpp \ + src/widgets/helper/NotebookButton.cpp \ + src/widgets/helper/NotebookTab.cpp \ + src/widgets/helper/ResizingTextEdit.cpp \ + src/widgets/helper/ScrollbarHighlight.cpp \ + src/widgets/helper/SearchPopup.cpp \ + src/widgets/helper/SettingsDialogTab.cpp \ + src/widgets/Notebook.cpp \ + src/widgets/Scrollbar.cpp \ + src/widgets/settingspages/AboutPage.cpp \ + src/widgets/settingspages/AccountsPage.cpp \ + src/widgets/settingspages/AdvancedPage.cpp \ + src/widgets/settingspages/BrowserExtensionPage.cpp \ + src/widgets/settingspages/CommandPage.cpp \ + src/widgets/settingspages/EmotesPage.cpp \ + src/widgets/settingspages/ExternalToolsPage.cpp \ + src/widgets/settingspages/FeelPage.cpp \ + src/widgets/settingspages/GeneralPage.cpp \ + src/widgets/settingspages/HighlightingPage.cpp \ + src/widgets/settingspages/IgnoresPage.cpp \ + src/widgets/settingspages/KeyboardSettingsPage.cpp \ + src/widgets/settingspages/LogsPage.cpp \ + src/widgets/settingspages/LookPage.cpp \ + src/widgets/settingspages/ModerationPage.cpp \ + src/widgets/settingspages/NotificationPage.cpp \ + src/widgets/settingspages/SettingsPage.cpp \ + src/widgets/settingspages/SpecialChannelsPage.cpp \ + src/widgets/splits/ClosedSplits.cpp \ + src/widgets/splits/Split.cpp \ + src/widgets/splits/SplitContainer.cpp \ + src/widgets/splits/SplitHeader.cpp \ + src/widgets/splits/SplitInput.cpp \ + src/widgets/splits/SplitOverlay.cpp \ + src/widgets/StreamView.cpp \ + src/widgets/Window.cpp \ + src/controllers/pings/PingController.cpp \ + src/controllers/pings/PingModel.cpp \ + +HEADERS += \ + src/Application.hpp \ + src/autogenerated/ResourcesAutogen.hpp \ + src/BrowserExtension.hpp \ + src/common/Aliases.hpp \ + src/common/Atomic.hpp \ + src/common/Channel.hpp \ + src/common/Common.hpp \ + src/common/CompletionModel.hpp \ + src/common/ConcurrentMap.hpp \ + src/common/DownloadManager.hpp \ + src/common/LinkParser.hpp \ + src/common/NetworkCommon.hpp \ + src/common/NetworkData.hpp \ + src/common/NetworkManager.hpp \ + src/common/NetworkRequest.hpp \ + src/common/NetworkRequester.hpp \ + src/common/NetworkResult.hpp \ + src/common/NetworkTimer.hpp \ + src/common/NetworkWorker.hpp \ + src/common/NullablePtr.hpp \ + src/common/ProviderId.hpp \ + src/common/SignalVector.hpp \ + src/common/SignalVectorModel.hpp \ + src/common/UniqueAccess.hpp \ + src/common/UsernameSet.hpp \ + src/common/Version.hpp \ + src/controllers/accounts/Account.hpp \ + src/controllers/accounts/AccountController.hpp \ + src/controllers/accounts/AccountModel.hpp \ + src/controllers/commands/Command.hpp \ + src/controllers/commands/CommandController.hpp \ + src/controllers/commands/CommandModel.hpp \ + src/controllers/highlights/HighlightBlacklistModel.hpp \ + src/controllers/highlights/HighlightBlacklistUser.hpp \ + src/controllers/highlights/HighlightController.hpp \ + src/controllers/highlights/HighlightModel.hpp \ + src/controllers/highlights/HighlightPhrase.hpp \ + src/controllers/highlights/UserHighlightModel.hpp \ + src/controllers/ignores/IgnoreController.hpp \ + src/controllers/ignores/IgnoreModel.hpp \ + src/controllers/ignores/IgnorePhrase.hpp \ + src/controllers/moderationactions/ModerationAction.hpp \ + src/controllers/moderationactions/ModerationActionModel.hpp \ + src/controllers/moderationactions/ModerationActions.hpp \ + src/controllers/notifications/NotificationController.hpp \ + src/controllers/notifications/NotificationModel.hpp \ + src/controllers/taggedusers/TaggedUser.hpp \ + src/controllers/taggedusers/TaggedUsersController.hpp \ + src/controllers/taggedusers/TaggedUsersModel.hpp \ + src/messages/Emote.hpp \ + src/messages/Image.hpp \ + src/messages/ImageSet.hpp \ + src/messages/layouts/MessageLayout.hpp \ + src/messages/layouts/MessageLayoutContainer.hpp \ + src/messages/layouts/MessageLayoutElement.hpp \ + src/messages/LimitedQueue.hpp \ + src/messages/LimitedQueueSnapshot.hpp \ + src/messages/Link.hpp \ + src/messages/Message.hpp \ + src/messages/MessageBuilder.hpp \ + src/messages/MessageColor.hpp \ + src/messages/MessageContainer.hpp \ + src/messages/MessageElement.hpp \ + src/messages/MessageParseArgs.hpp \ + src/messages/Selection.hpp \ + src/PrecompiledHeader.hpp \ + src/providers/bttv/BttvEmotes.hpp \ + src/providers/bttv/LoadBttvChannelEmote.hpp \ + src/providers/chatterino/ChatterinoBadges.hpp \ + src/providers/emoji/Emojis.hpp \ + src/providers/ffz/FfzEmotes.hpp \ + src/providers/ffz/FfzModBadge.hpp \ + src/providers/irc/AbstractIrcServer.hpp \ + src/providers/irc/IrcAccount.hpp \ + src/providers/irc/IrcChannel2.hpp \ + src/providers/irc/IrcConnection2.hpp \ + src/providers/irc/IrcServer.hpp \ + src/providers/LinkResolver.hpp \ + src/providers/twitch/ChatroomChannel.hpp \ + src/providers/twitch/EmoteValue.hpp \ + src/providers/twitch/IrcMessageHandler.hpp \ + src/providers/twitch/PartialTwitchUser.hpp \ + src/providers/twitch/PubsubActions.hpp \ + src/providers/twitch/PubsubClient.hpp \ + src/providers/twitch/PubsubHelpers.hpp \ + src/providers/twitch/TwitchAccount.hpp \ + src/providers/twitch/TwitchAccountManager.hpp \ + src/providers/twitch/TwitchApi.hpp \ + src/providers/twitch/TwitchBadges.hpp \ + src/providers/twitch/TwitchChannel.hpp \ + src/providers/twitch/TwitchCommon.hpp \ + src/providers/twitch/TwitchEmotes.hpp \ + src/providers/twitch/TwitchHelpers.hpp \ + src/providers/twitch/TwitchMessageBuilder.hpp \ + src/providers/twitch/TwitchParseCheerEmotes.hpp \ + src/providers/twitch/TwitchServer.hpp \ + src/providers/twitch/TwitchUser.hpp \ + src/RunGui.hpp \ + src/singletons/TooltipPreviewImage.hpp \ + src/singletons/Badges.hpp \ + src/singletons/Emotes.hpp \ + src/singletons/helper/GifTimer.hpp \ + src/singletons/helper/LoggingChannel.hpp \ + src/singletons/Logging.hpp \ + src/singletons/NativeMessaging.hpp \ + src/singletons/Paths.hpp \ + src/singletons/Resources.hpp \ + src/singletons/Settings.hpp \ + src/singletons/Theme.hpp \ + src/singletons/Toasts.hpp \ + src/singletons/Updates.hpp \ + src/singletons/WindowManager.hpp \ + src/util/ConcurrentMap.hpp \ + src/util/DebugCount.hpp \ + src/util/FormatTime.hpp \ + src/util/IncognitoBrowser.hpp \ + src/util/InitUpdateButton.hpp \ + src/util/IrcHelpers.hpp \ + src/util/IsBigEndian.hpp \ + src/util/JsonQuery.hpp \ + src/util/LayoutCreator.hpp \ + src/util/QStringHash.hpp \ + src/util/rangealgorithm.hpp \ + src/util/RapidjsonHelpers.hpp \ + src/util/RemoveScrollAreaBackground.hpp \ + src/util/SharedPtrElementLess.hpp \ + src/util/StandardItemHelper.hpp \ + src/util/StreamLink.hpp \ + src/widgets/AccountSwitchPopupWidget.hpp \ + src/widgets/AccountSwitchWidget.hpp \ + src/widgets/AttachedWindow.hpp \ + src/widgets/dialogs/EmotePopup.hpp \ + src/widgets/dialogs/LastRunCrashDialog.hpp \ + src/widgets/dialogs/LoginDialog.hpp \ + src/widgets/dialogs/LogsPopup.hpp \ + src/widgets/dialogs/NotificationPopup.hpp \ + src/widgets/dialogs/QualityPopup.hpp \ + src/widgets/dialogs/SelectChannelDialog.hpp \ + src/widgets/dialogs/SettingsDialog.hpp \ + src/widgets/dialogs/TextInputDialog.hpp \ + src/widgets/dialogs/UpdateDialog.hpp \ + src/widgets/dialogs/UserInfoPopup.hpp \ + src/widgets/dialogs/WelcomeDialog.hpp \ + src/widgets/helper/ChannelView.hpp \ + src/widgets/helper/ComboBoxItemDelegate.hpp \ + src/widgets/helper/CommonTexts.hpp \ + src/widgets/helper/DebugPopup.hpp \ + src/widgets/helper/EditableModelView.hpp \ + src/widgets/helper/Line.hpp \ + src/widgets/helper/NotebookButton.hpp \ + src/widgets/helper/NotebookTab.hpp \ + src/widgets/helper/ResizingTextEdit.hpp \ + src/widgets/helper/ScrollbarHighlight.hpp \ + src/widgets/helper/SearchPopup.hpp \ + src/widgets/helper/SettingsDialogTab.hpp \ + src/widgets/Notebook.hpp \ + src/widgets/Scrollbar.hpp \ + src/widgets/settingspages/AboutPage.hpp \ + src/widgets/settingspages/AccountsPage.hpp \ + src/widgets/settingspages/AdvancedPage.hpp \ + src/widgets/settingspages/BrowserExtensionPage.hpp \ + src/widgets/settingspages/CommandPage.hpp \ + src/widgets/settingspages/EmotesPage.hpp \ + src/widgets/settingspages/ExternalToolsPage.hpp \ + src/widgets/settingspages/FeelPage.hpp \ + src/widgets/settingspages/GeneralPage.hpp \ + src/widgets/settingspages/HighlightingPage.hpp \ + src/widgets/settingspages/IgnoresPage.hpp \ + src/widgets/settingspages/KeyboardSettingsPage.hpp \ + src/widgets/settingspages/LogsPage.hpp \ + src/widgets/settingspages/LookPage.hpp \ + src/widgets/settingspages/ModerationPage.hpp \ + src/widgets/settingspages/NotificationPage.hpp \ + src/widgets/settingspages/SettingsPage.hpp \ + src/widgets/settingspages/SpecialChannelsPage.hpp \ + src/widgets/splits/ClosedSplits.hpp \ + src/widgets/splits/Split.hpp \ + src/widgets/splits/SplitContainer.hpp \ + src/widgets/splits/SplitHeader.hpp \ + src/widgets/splits/SplitInput.hpp \ + src/widgets/splits/SplitOverlay.hpp \ + src/widgets/StreamView.hpp \ + src/widgets/Window.hpp \ + src/controllers/pings/PingController.hpp \ + src/controllers/pings/PingModel.hpp \ + +RESOURCES += \ + resources/resources.qrc \ + resources/resources_autogenerated.qrc + +DISTFILES += + +FORMS += + +# do not use windows min/max macros +#win32 { +# DEFINES += NOMINMAX +#} + +linux:isEmpty(PREFIX) { + message("Using default installation prefix (/usr/local). Change PREFIX in qmake command") + PREFIX = /usr/local +} + +linux { + desktop.files = resources/chatterino.desktop + desktop.path = $$PREFIX/share/applications + + build_icons.path = . + build_icons.commands = @echo $$PWD && mkdir -p $$PWD/resources/linuxinstall/icons/hicolor/256x256 && cp $$PWD/resources/icon.png $$PWD/resources/linuxinstall/icons/hicolor/256x256/chatterino.png + + icon.files = $$PWD/resources/linuxinstall/icons/hicolor/256x256/chatterino.png + icon.path = $$PREFIX/share/icons/hicolor/256x256/apps + + target.path = $$PREFIX/bin + + INSTALLS += desktop build_icons icon target +} diff --git a/docs/ENV.md b/docs/ENV.md new file mode 100644 index 000000000..586aac354 --- /dev/null +++ b/docs/ENV.md @@ -0,0 +1,20 @@ +# Environment variables +Below I have tried to list all environment variables that can be used to modify the behaviour of Chatterino. Used for things that I don't feel like fit in the settings system. + +### CHATTERINO2_RECENT_MESSAGES_URL +Used to change the URL that Chatterino2 uses when trying to load historic Twitch chat messages (if the setting is enabled). +Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1?clearchatToNotice=true` +Arguments: + - `%1` = Name of the Twitch channel + +### CHATTERINO2_LINK_RESOLVER_URL +Used to change the URL that Chatterino2 uses when trying to get link information to display in the tooltip on hover. +Default value: `https://braize.pajlada.com/chatterino/link_resolver/%1` +Arguments: + - `%1` = Escaped URL the link resolver should resolve + +### CHATTERINO2_TWITCH_EMOTE_SET_RESOLVER_URL +Used to change the URL that Chatterino2 uses when trying to get emote set information +Default value: `https://braize.pajlada.com/chatterino/twitchemotes/set/%1/` +Arguments: + - `%1` = Emote set ID diff --git a/lib/appbase b/lib/appbase index e6a31d522..d05492573 160000 --- a/lib/appbase +++ b/lib/appbase @@ -1 +1 @@ -Subproject commit e6a31d5228ed8969596d6fdbd030f71a7a17f30d +Subproject commit d054925734cf26576346a1da856f7ab0d4b6c0a5 diff --git a/resources/.gitignore b/resources/.gitignore new file mode 100644 index 000000000..1d38bf357 --- /dev/null +++ b/resources/.gitignore @@ -0,0 +1 @@ +linuxinstall diff --git a/resources/buttons/trashCan.png b/resources/buttons/trashCan.png new file mode 100644 index 000000000..fd1bce3a0 Binary files /dev/null and b/resources/buttons/trashCan.png differ diff --git a/resources/buttons/trashcan.svg b/resources/buttons/trashcan.svg new file mode 100644 index 000000000..e225ee4ea --- /dev/null +++ b/resources/buttons/trashcan.svg @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + Trashcan top + + + + + + + diff --git a/resources/chatterino.desktop b/resources/chatterino.desktop new file mode 100644 index 000000000..23cc8c356 --- /dev/null +++ b/resources/chatterino.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=Chatterino +Comment=Chat client for Twitch +Exec=chatterino +Icon=chatterino +Terminal=false +Categories=Network;InstantMessaging; diff --git a/resources/chatterino2.icns b/resources/chatterino.icns similarity index 100% rename from resources/chatterino2.icns rename to resources/chatterino.icns diff --git a/resources/generate_resources.py b/resources/generate_resources.py index 5239e64e8..e48795f72 100755 --- a/resources/generate_resources.py +++ b/resources/generate_resources.py @@ -8,7 +8,7 @@ ignored_files = ['qt.conf', 'resources.qrc', 'resources_autogenerated.qrc', 'win # to ignore all files in a/b, add a/b to ignored_directories. # this will ignore a/b/c/d.txt and a/b/xd.txt -ignored_directories = ['__pycache__'] +ignored_directories = ['__pycache__', 'linuxinstall'] def isNotIgnored(file): # check if file exists in an ignored direcory diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index fcf0b5dac..96204e348 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -1,5 +1,5 @@ - chatterino2.icns + chatterino.icns contributors.txt emoji.json emojidata.txt @@ -24,6 +24,7 @@ buttons/modModeEnabled.png buttons/modModeEnabled2.png buttons/timeout.png + buttons/trashCan.png buttons/unban.png buttons/unmod.png buttons/update.png diff --git a/src/Application.cpp b/src/Application.cpp index 2870c6ed6..42067d9bd 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -6,6 +6,7 @@ #include "controllers/ignores/IgnoreController.hpp" #include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/notifications/NotificationController.hpp" +#include "controllers/pings/PingController.hpp" #include "controllers/taggedusers/TaggedUsersController.hpp" #include "debug/Log.hpp" #include "messages/MessageBuilder.hpp" @@ -53,6 +54,7 @@ Application::Application(Settings &_settings, Paths &_paths) , commands(&this->emplace()) , highlights(&this->emplace()) , notifications(&this->emplace()) + , pings(&this->emplace()) , ignores(&this->emplace()) , taggedUsers(&this->emplace()) , moderationActions(&this->emplace()) @@ -264,6 +266,7 @@ void Application::initPubsub() auto msg = MessageBuilder(action).release(); postToThread([chan, msg] { chan->addMessage(msg); }); + chan->deleteMessage(msg->id); }); this->twitch.pubsub->start(); diff --git a/src/Application.hpp b/src/Application.hpp index c872a4c62..49c8fe96c 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -18,6 +18,7 @@ class TaggedUsersController; class AccountController; class ModerationActions; class NotificationController; +class PingController; class Theme; class WindowManager; @@ -62,6 +63,7 @@ public: CommandController *const commands{}; HighlightController *const highlights{}; NotificationController *const notifications{}; + PingController *const pings{}; IgnoreController *const ignores{}; TaggedUsersController *const taggedUsers{}; ModerationActions *const moderationActions{}; diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp index 34cf039ef..59f5fce70 100644 --- a/src/autogenerated/ResourcesAutogen.cpp +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -18,6 +18,7 @@ Resources2::Resources2() this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png"); this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png"); this->buttons.timeout = QPixmap(":/buttons/timeout.png"); + this->buttons.trashCan = QPixmap(":/buttons/trashCan.png"); this->buttons.unban = QPixmap(":/buttons/unban.png"); this->buttons.unmod = QPixmap(":/buttons/unmod.png"); this->buttons.update = QPixmap(":/buttons/update.png"); diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp index fbbb2fbf3..1c8a0bce4 100644 --- a/src/autogenerated/ResourcesAutogen.hpp +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -24,6 +24,7 @@ public: QPixmap modModeEnabled; QPixmap modModeEnabled2; QPixmap timeout; + QPixmap trashCan; QPixmap unban; QPixmap unmod; QPixmap update; diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 8d247cf2f..caa432752 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -78,7 +78,7 @@ void Channel::addMessage(MessagePtr message, // FOURTF: change this when adding more providers if (this->isTwitchChannel() && - (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))) + (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))) { app->logging->addMessage(this->name_, message); } @@ -154,8 +154,9 @@ void Channel::addOrReplaceTimeout(MessagePtr message) for (int i = 0; i < snapshotLength; i++) { auto &s = snapshot[i]; - if (s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout}) && - s->loginName == message->timeoutUser) + if (s->loginName == message->timeoutUser && + s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout, + MessageFlag::Whisper})) { // FOURTF: disabled for now // PAJLADA: Shitty solution described in Message.hpp @@ -179,7 +180,8 @@ void Channel::disableAllMessages() for (int i = 0; i < snapshotLength; i++) { auto &message = snapshot[i]; - if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout})) + if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout, + MessageFlag::Whisper})) { continue; } @@ -210,6 +212,25 @@ void Channel::replaceMessage(MessagePtr message, MessagePtr replacement) } } +void Channel::deleteMessage(QString messageID) +{ + LimitedQueueSnapshot snapshot = this->getMessageSnapshot(); + int snapshotLength = snapshot.size(); + + int end = std::max(0, snapshotLength - 200); + + for (int i = snapshotLength - 1; i >= end; --i) + { + auto &s = snapshot[i]; + + if (s->id == messageID) + { + s->flags.set(MessageFlag::Disabled); + break; + } + } +} + void Channel::addRecentChatter(const MessagePtr &message) { } @@ -239,6 +260,11 @@ bool Channel::hasModRights() const return this->isMod() || this->isBroadcaster(); } +bool Channel::hasHighRateLimit() const +{ + return this->isMod() || this->isBroadcaster(); +} + bool Channel::isLive() const { return false; diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp index 9e111aa46..9abc668ea 100644 --- a/src/common/Channel.hpp +++ b/src/common/Channel.hpp @@ -15,7 +15,7 @@ namespace chatterino { struct Message; using MessagePtr = std::shared_ptr; -enum class MessageFlag : uint16_t; +enum class MessageFlag : uint32_t; using MessageFlags = FlagsEnum; class Channel : public std::enable_shared_from_this @@ -62,6 +62,7 @@ public: void addOrReplaceTimeout(MessagePtr message); void disableAllMessages(); void replaceMessage(MessagePtr message, MessagePtr replacement); + void deleteMessage(QString messageID); QStringList modList; @@ -70,6 +71,7 @@ public: virtual bool isMod() const; virtual bool isBroadcaster() const; virtual bool hasModRights() const; + virtual bool hasHighRateLimit() const; virtual bool isLive() const; virtual bool shouldIgnoreHighlights() const; diff --git a/src/common/DownloadManager.cpp b/src/common/DownloadManager.cpp index 286150a7c..5adc8ef32 100644 --- a/src/common/DownloadManager.cpp +++ b/src/common/DownloadManager.cpp @@ -20,9 +20,7 @@ DownloadManager::~DownloadManager() void DownloadManager::setFile(QString fileURL, const QString &channelName) { - QString filePath = fileURL; QString saveFilePath; - QStringList filePathList = filePath.split('/'); saveFilePath = getPaths()->twitchProfileAvatars + "/twitch/" + channelName + ".png"; QNetworkRequest request; diff --git a/src/common/Env.cpp b/src/common/Env.cpp new file mode 100644 index 000000000..ff711ec47 --- /dev/null +++ b/src/common/Env.cpp @@ -0,0 +1,40 @@ +#include "common/Env.hpp" + +namespace chatterino { + +namespace { + + QString readStringEnv(const char *envName, QString defaultValue) + { + auto envString = std::getenv(envName); + if (envString != nullptr) + { + return QString(envString); + } + + return defaultValue; + } + +} // namespace + +Env::Env() + : recentMessagesApiUrl( + readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL", + "https://recent-messages.robotty.de/api/v2/" + "recent-messages/%1?clearchatToNotice=true")) + , linkResolverUrl(readStringEnv( + "CHATTERINO2_LINK_RESOLVER_URL", + "https://braize.pajlada.com/chatterino/link_resolver/%1")) + , twitchEmoteSetResolverUrl(readStringEnv( + "CHATTERINO2_TWITCH_EMOTE_SET_RESOLVER_URL", + "https://braize.pajlada.com/chatterino/twitchemotes/set/%1/")) +{ +} + +const Env &Env::get() +{ + static Env instance; + return instance; +} + +} // namespace chatterino diff --git a/src/common/Env.hpp b/src/common/Env.hpp new file mode 100644 index 000000000..2dc7fa1ba --- /dev/null +++ b/src/common/Env.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace chatterino { + +class Env +{ + Env(); + +public: + static const Env &get(); + + const QString recentMessagesApiUrl; + const QString linkResolverUrl; + const QString twitchEmoteSetResolverUrl; +}; + +} // namespace chatterino diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp index 187356dea..d79263830 100644 --- a/src/common/SignalVectorModel.hpp +++ b/src/common/SignalVectorModel.hpp @@ -188,7 +188,7 @@ public: assert(row >= 0 && row < this->rows_.size() && column >= 0 && column < this->columnCount_); - return this->rows_[index.row()].items[index.column()]->flags(); + return this->rows_[row].items[column]->flags(); } QStandardItem *getItem(int row, int column) diff --git a/src/common/UsernameSet.cpp b/src/common/UsernameSet.cpp index 8e0a53af7..964b6c0a7 100644 --- a/src/common/UsernameSet.cpp +++ b/src/common/UsernameSet.cpp @@ -59,7 +59,7 @@ void UsernameSet::insertPrefix(const QString &value) { auto &string = this->firstKeyForPrefix[Prefix(value)]; - if (string.isNull() || value < string) + if (string.isNull() || value.compare(string, Qt::CaseInsensitive) < 0) string = value; } @@ -99,6 +99,11 @@ bool Prefix::operator==(const Prefix &other) const std::tie(other.first, other.second); } +bool Prefix::operator!=(const Prefix &other) const +{ + return !(*this == other); +} + bool Prefix::isStartOf(const QString &string) const { if (string.size() == 0) diff --git a/src/common/UsernameSet.hpp b/src/common/UsernameSet.hpp index d71bcdaf6..f07911513 100644 --- a/src/common/UsernameSet.hpp +++ b/src/common/UsernameSet.hpp @@ -6,11 +6,13 @@ #include namespace chatterino { + class Prefix { public: Prefix(const QString &string); bool operator==(const Prefix &other) const; + bool operator!=(const Prefix &other) const; bool isStartOf(const QString &string) const; private: @@ -19,9 +21,11 @@ private: friend struct std::hash; }; + } // namespace chatterino namespace std { + template <> struct hash { size_t operator()(const chatterino::Prefix &prefix) const @@ -30,9 +34,18 @@ struct hash { size_t(prefix.second.unicode()); } }; + } // namespace std namespace chatterino { + +struct CaseInsensitiveLess { + bool operator()(const QString &lhs, const QString &rhs) const + { + return lhs.compare(rhs, Qt::CaseInsensitive) < 0; + } +}; + class UsernameSet { public: @@ -66,7 +79,7 @@ public: private: void insertPrefix(const QString &string); - std::set items; + std::set items; std::unordered_map firstKeyForPrefix; }; diff --git a/src/controllers/accounts/AccountModel.cpp b/src/controllers/accounts/AccountModel.cpp index 6e1d2a5df..875efbdc5 100644 --- a/src/controllers/accounts/AccountModel.cpp +++ b/src/controllers/accounts/AccountModel.cpp @@ -31,12 +31,12 @@ int AccountModel::beforeInsert(const std::shared_ptr &item, { if (this->categoryCount_[item->getCategory()]++ == 0) { - auto row = this->createRow(); + auto newRow = this->createRow(); - setStringItem(row[0], item->getCategory(), false, false); - row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole); + setStringItem(newRow[0], item->getCategory(), false, false); + newRow[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole); - this->insertCustomRow(std::move(row), proposedIndex); + this->insertCustomRow(std::move(newRow), proposedIndex); return proposedIndex + 1; } diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 8d98a999a..ae46f6e2e 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -15,6 +15,7 @@ #include "singletons/Emotes.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" +#include "singletons/Theme.hpp" #include "util/CombinePath.hpp" #include "widgets/dialogs/LogsPopup.hpp" @@ -28,9 +29,143 @@ "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \ "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \ "/clear", "/subscribers", "/subscribersoff", "/followers", \ - "/followersoff" \ + "/followersoff", "/user" \ } +namespace { +using namespace chatterino; + +static const QStringList whisperCommands{"/w", ".w"}; + +void sendWhisperMessage(const QString &text) +{ + // (hemirt) pajlada: "we should not be sending whispers through jtv, but + // rather to your own username" + auto app = getApp(); + app->twitch.server->sendMessage("jtv", text.simplified()); +} + +bool appendWhisperMessageWordsLocally(const QStringList &words) +{ + auto app = getApp(); + + MessageBuilder b; + + b.emplace(); + b.emplace(app->accounts->twitch.getCurrent()->getUserName(), + MessageElementFlag::Text, MessageColor::Text, + FontStyle::ChatMediumBold); + b.emplace("->", MessageElementFlag::Text, + getApp()->themes->messages.textColors.system); + b.emplace(words[1] + ":", MessageElementFlag::Text, + MessageColor::Text, FontStyle::ChatMediumBold); + + const auto &acc = app->accounts->twitch.getCurrent(); + const auto &accemotes = *acc->accessEmotes(); + const auto &bttvemotes = app->twitch.server->getBttvEmotes(); + const auto &ffzemotes = app->twitch.server->getFfzEmotes(); + auto flags = MessageElementFlags(); + auto emote = boost::optional{}; + for (int i = 2; i < words.length(); i++) + { + { // twitch emote + auto it = accemotes.emotes.find({words[i]}); + if (it != accemotes.emotes.end()) + { + b.emplace(it->second, + MessageElementFlag::TwitchEmote); + continue; + } + } // twitch emote + + { // bttv/ffz emote + if ((emote = bttvemotes.emote({words[i]}))) + { + flags = MessageElementFlag::BttvEmote; + } + else if ((emote = ffzemotes.emote({words[i]}))) + { + flags = MessageElementFlag::FfzEmote; + } + if (emote) + { + b.emplace(emote.get(), flags); + continue; + } + } // bttv/ffz emote + { // emoji/text + for (auto &variant : app->emotes->emojis.parse(words[i])) + { + constexpr const static struct { + void operator()(EmotePtr emote, MessageBuilder &b) const + { + b.emplace(emote, + MessageElementFlag::EmojiAll); + } + void operator()(const QString &string, + MessageBuilder &b) const + { + auto linkString = b.matchLink(string); + if (linkString.isEmpty()) + { + b.emplace(string, + MessageElementFlag::Text); + } + else + { + b.addLink(string, linkString); + } + } + } visitor; + boost::apply_visitor([&b](auto &&arg) { visitor(arg, b); }, + variant); + } // emoji/text + } + } + + b->flags.set(MessageFlag::DoNotTriggerNotification); + b->flags.set(MessageFlag::Whisper); + auto messagexD = b.release(); + + app->twitch.server->whispersChannel->addMessage(messagexD); + + auto overrideFlags = boost::optional(messagexD->flags); + overrideFlags->set(MessageFlag::DoNotLog); + + if (getSettings()->inlineWhispers) + { + app->twitch.server->forEachChannel( + [&messagexD, overrideFlags](ChannelPtr _channel) { + _channel->addMessage(messagexD, overrideFlags); + }); + } + + return true; +} + +bool appendWhisperMessageStringLocally(const QString &textNoEmoji) +{ + QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji); + QStringList words = text.split(' ', QString::SkipEmptyParts); + + if (words.length() == 0) + { + return false; + } + + QString commandName = words[0]; + + if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) + { + if (words.length() > 2) + { + return appendWhisperMessageWordsLocally(words); + } + } + return false; +} +} // namespace + namespace chatterino { void CommandController::initialize(Settings &, Paths &paths) @@ -122,99 +257,12 @@ QString CommandController::execCommand(const QString &textNoEmoji, // works in a valid twitch channel and /whispers, etc... if (!dryRun && channel->isTwitchChannel()) { - if (commandName == "/w") + if (whisperCommands.contains(commandName, Qt::CaseInsensitive)) { - if (words.length() <= 2) + if (words.length() > 2) { - return ""; - } - - auto app = getApp(); - - MessageBuilder b; - - b.emplace(); - 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); - - const auto &acc = app->accounts->twitch.getCurrent(); - const auto &accemotes = *acc->accessEmotes(); - const auto &bttvemotes = app->twitch.server->getBttvEmotes(); - const auto &ffzemotes = app->twitch.server->getFfzEmotes(); - auto flags = MessageElementFlags(); - auto emote = boost::optional{}; - for (int i = 2; i < words.length(); i++) - { - { // twitch emote - auto it = accemotes.emotes.find({words[i]}); - if (it != accemotes.emotes.end()) - { - b.emplace( - it->second, MessageElementFlag::TwitchEmote); - continue; - } - } // twitch emote - - { // bttv/ffz emote - if ((emote = bttvemotes.emote({words[i]}))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = ffzemotes.emote({words[i]}))) - { - flags = MessageElementFlag::FfzEmote; - } - if (emote) - { - b.emplace(emote.get(), flags); - continue; - } - } // bttv/ffz emote - { // emoji/text - for (auto &variant : app->emotes->emojis.parse(words[i])) - { - constexpr const static struct { - void operator()(EmotePtr emote, - MessageBuilder &b) const - { - b.emplace( - emote, MessageElementFlag::EmojiAll); - } - void operator()(const QString &string, - MessageBuilder &b) const - { - b.emplace( - string, MessageElementFlag::Text); - } - } visitor; - boost::apply_visitor( - [&b](auto &&arg) { visitor(arg, b); }, variant); - } // emoji/text - } - } - - b->flags.set(MessageFlag::DoNotTriggerNotification); - auto messagexD = b.release(); - - app->twitch.server->whispersChannel->addMessage(messagexD); - - app->twitch.server->sendMessage("jtv", text); - - auto overrideFlags = boost::optional(messagexD->flags); - overrideFlags->set(MessageFlag::DoNotLog); - - if (getSettings()->inlineWhispers) - { - app->twitch.server->forEachChannel( - [&messagexD, overrideFlags](ChannelPtr _channel) { - _channel->addMessage(messagexD, overrideFlags); - }); + appendWhisperMessageWordsLocally(words); + sendWhisperMessage(text); } return ""; @@ -413,13 +461,36 @@ QString CommandController::execCommand(const QString &textNoEmoji, logs->show(); return ""; } + else if (commandName == "/user") + { + if (words.size() < 2) + { + channel->addMessage( + makeSystemMessage("Usage /user [user] (channel)")); + return ""; + } + QString channelName = channel->getName(); + if (words.size() > 2) + { + channelName = words[2]; + if (channelName[0] == '#') + { + channelName.remove(0, 1); + } + } + QDesktopServices::openUrl("https://www.twitch.tv/popout/" + + channelName + "/viewercard/" + words[1]); + return ""; + } } - // check if custom command exists - auto it = this->commandsMap_.find(commandName); - if (it != this->commandsMap_.end()) { - return this->execCustomCommand(words, it.value()); + // check if custom command exists + const auto it = this->commandsMap_.find(commandName); + if (it != this->commandsMap_.end()) + { + return this->execCustomCommand(words, it.value(), dryRun); + } } auto maxSpaces = std::min(this->maxSpaces_, words.length() - 1); @@ -427,10 +498,10 @@ QString CommandController::execCommand(const QString &textNoEmoji, { commandName += ' ' + words[i + 1]; - auto it = this->commandsMap_.find(commandName); + const auto it = this->commandsMap_.find(commandName); if (it != this->commandsMap_.end()) { - return this->execCustomCommand(words, it.value()); + return this->execCustomCommand(words, it.value(), dryRun); } } @@ -438,7 +509,8 @@ QString CommandController::execCommand(const QString &textNoEmoji, } QString CommandController::execCustomCommand(const QStringList &words, - const Command &command) + const Command &command, + bool dryRun) { QString result; @@ -509,7 +581,17 @@ QString CommandController::execCustomCommand(const QStringList &words, result = result.mid(1); } - return result.replace("{{", "{"); + auto res = result.replace("{{", "{"); + + if (dryRun || !appendWhisperMessageStringLocally(res)) + { + return res; + } + else + { + sendWhisperMessage(res); + return ""; + } } QStringList CommandController::getDefaultTwitchCommandList() diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp index fb6f9d89d..e29ee25a9 100644 --- a/src/controllers/commands/CommandController.hpp +++ b/src/controllers/commands/CommandController.hpp @@ -49,7 +49,8 @@ private: std::unique_ptr>> commandsSetting_; - QString execCustomCommand(const QStringList &words, const Command &command); + QString execCustomCommand(const QStringList &words, const Command &command, + bool dryRun); }; } // namespace chatterino diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp index 10c83ac10..1279f4a88 100644 --- a/src/controllers/ignores/IgnoreController.hpp +++ b/src/controllers/ignores/IgnoreController.hpp @@ -12,6 +12,8 @@ class Paths; class IgnoreModel; +enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster }; + class IgnoreController final : public Singleton { public: diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index 0b6418a57..199ea5521 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -73,6 +73,10 @@ ModerationAction::ModerationAction(const QString &action) { this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban); } + else if (action.startsWith("/delete")) + { + this->image_ = Image::fromPixmap(getApp()->resources->buttons.trashCan); + } else { QString xD = action; diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 395de1974..081084fba 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -103,16 +103,12 @@ void NotificationController::playSound() static auto player = new QMediaPlayer; static QUrl currentPlayerUrl; - QUrl highlightSoundUrl; - if (getSettings()->notificationCustomSound) - { - highlightSoundUrl = QUrl::fromLocalFile( - getSettings()->notificationPathSound.getValue()); - } - else - { - highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav"); - } + QUrl highlightSoundUrl = + getSettings()->notificationCustomSound + ? QUrl::fromLocalFile( + getSettings()->notificationPathSound.getValue()) + : QUrl("qrc:/sounds/ping2.wav"); + if (currentPlayerUrl != highlightSoundUrl) { player->setMedia(highlightSoundUrl); diff --git a/src/controllers/pings/PingController.cpp b/src/controllers/pings/PingController.cpp new file mode 100644 index 000000000..0f0ed37d8 --- /dev/null +++ b/src/controllers/pings/PingController.cpp @@ -0,0 +1,70 @@ +#include "controllers/pings/PingController.hpp" +#include "controllers/pings/PingModel.hpp" + +namespace chatterino { + +void PingController::initialize(Settings &settings, Paths &paths) +{ + this->initialized_ = true; + for (const QString &channelName : this->pingSetting_.getValue()) + { + this->channelVector.appendItem(channelName); + } + + this->channelVector.delayedItemsChanged.connect([this] { // + this->pingSetting_.setValue(this->channelVector.getVector()); + }); +} + +PingModel *PingController::createModel(QObject *parent) +{ + PingModel *model = new PingModel(parent); + model->init(&this->channelVector); + return model; +} + +bool PingController::isMuted(const QString &channelName) +{ + for (const auto &channel : this->channelVector.getVector()) + { + if (channelName.toLower() == channel.toLower()) + { + return true; + } + } + return false; +} + +void PingController::muteChannel(const QString &channelName) +{ + channelVector.appendItem(channelName); +} + +void PingController::unmuteChannel(const QString &channelName) +{ + for (std::vector::size_type i = 0; + i != channelVector.getVector().size(); i++) + { + if (channelVector.getVector()[i].toLower() == channelName.toLower()) + { + channelVector.removeItem(i); + i--; + } + } +} + +bool PingController::toggleMuteChannel(const QString &channelName) +{ + if (this->isMuted(channelName)) + { + unmuteChannel(channelName); + return false; + } + else + { + muteChannel(channelName); + return true; + } +} + +} // namespace chatterino diff --git a/src/controllers/pings/PingController.hpp b/src/controllers/pings/PingController.hpp new file mode 100644 index 000000000..80805e856 --- /dev/null +++ b/src/controllers/pings/PingController.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "common/SignalVector.hpp" +#include "common/Singleton.hpp" +#include "singletons/Settings.hpp" + +namespace chatterino { + +class Settings; +class Paths; + +class PingModel; + +class PingController final : public Singleton, private QObject +{ +public: + virtual void initialize(Settings &settings, Paths &paths) override; + + bool isMuted(const QString &channelName); + void muteChannel(const QString &channelName); + void unmuteChannel(const QString &channelName); + bool toggleMuteChannel(const QString &channelName); + + PingModel *createModel(QObject *parent); + +private: + bool initialized_ = false; + + UnsortedSignalVector channelVector; + + ChatterinoSetting> pingSetting_ = {"/pings/muted"}; +}; + +} // namespace chatterino diff --git a/src/controllers/pings/PingModel.cpp b/src/controllers/pings/PingModel.cpp new file mode 100644 index 000000000..28098a209 --- /dev/null +++ b/src/controllers/pings/PingModel.cpp @@ -0,0 +1,28 @@ +#include "PingModel.hpp" + +#include "Application.hpp" +#include "singletons/Settings.hpp" +#include "util/StandardItemHelper.hpp" + +namespace chatterino { + +PingModel::PingModel(QObject *parent) + : SignalVectorModel(1, parent) +{ +} + +// turn a vector item into a model row +QString PingModel::getItemFromRow(std::vector &row, + const QString &original) +{ + return QString(row[0]->data(Qt::DisplayRole).toString()); +} + +// turn a model +void PingModel::getRowFromItem(const QString &item, + std::vector &row) +{ + setStringItem(row[0], item); +} + +} // namespace chatterino diff --git a/src/controllers/pings/PingModel.hpp b/src/controllers/pings/PingModel.hpp new file mode 100644 index 000000000..137be5e0c --- /dev/null +++ b/src/controllers/pings/PingModel.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "common/SignalVectorModel.hpp" +#include "controllers/notifications/NotificationController.hpp" + +namespace chatterino { + +class PingController; + +class PingModel : public SignalVectorModel +{ + explicit PingModel(QObject *parent); + +protected: + // turn a vector item into a model row + virtual QString getItemFromRow(std::vector &row, + const QString &original) override; + + // turns a row in the model into a vector item + virtual void getRowFromItem(const QString &item, + std::vector &row) override; + + friend class PingController; +}; + +} // namespace chatterino diff --git a/src/messages/HistoricMessageAppearance.hpp b/src/messages/HistoricMessageAppearance.hpp deleted file mode 100644 index 54cdb0686..000000000 --- a/src/messages/HistoricMessageAppearance.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -namespace chatterino { - -enum HistoricMessageAppearance { - Crossed = (1 << 0), - Greyed = (1 << 1), -}; - -} // namespace chatterino diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp index 3142d8eca..61e8db56f 100644 --- a/src/messages/LimitedQueue.hpp +++ b/src/messages/LimitedQueue.hpp @@ -28,8 +28,8 @@ template class LimitedQueue { protected: - typedef std::shared_ptr> Chunk; - typedef std::shared_ptr> ChunkVector; + using Chunk = std::vector; + using ChunkVector = std::vector>; public: LimitedQueue(size_t limit = 1000) @@ -42,9 +42,8 @@ public: { std::lock_guard lock(this->mutex_); - this->chunks_ = - std::make_shared>>>(); - Chunk chunk = std::make_shared>(); + this->chunks_ = std::make_shared(); + auto chunk = std::make_shared(); chunk->resize(this->chunkSize_); this->chunks_->push_back(chunk); this->firstChunkOffset_ = 0; @@ -57,23 +56,21 @@ public: { std::lock_guard lock(this->mutex_); - Chunk lastChunk = this->chunks_->back(); + auto lastChunk = this->chunks_->back(); - // still space in the last chunk if (lastChunk->size() <= this->lastChunkEnd_) { - // create new chunk vector - ChunkVector newVector = std::make_shared< - std::vector>>>(); + // Last chunk is full, create a new one and rebuild our chunk vector + auto newVector = std::make_shared(); // copy chunks - for (Chunk &chunk : *this->chunks_) + for (auto &chunk : *this->chunks_) { newVector->push_back(chunk); } // push back new chunk - Chunk newChunk = std::make_shared>(); + auto newChunk = std::make_shared(); newChunk->resize(this->chunkSize_); newVector->push_back(newChunk); @@ -98,8 +95,7 @@ public: std::lock_guard lock(this->mutex_); // create new vector to clone chunks into - ChunkVector newChunks = std::make_shared< - std::vector>>>(); + auto newChunks = std::make_shared(); newChunks->resize(this->chunks_->size()); @@ -112,7 +108,7 @@ public: // create new chunk for the first one size_t offset = std::min(this->space(), static_cast(items.size())); - Chunk newFirstChunk = std::make_shared>(); + auto newFirstChunk = std::make_shared(); newFirstChunk->resize(this->chunks_->front()->size() + offset); for (size_t i = 0; i < offset; i++) @@ -150,7 +146,7 @@ public: for (size_t i = 0; i < this->chunks_->size(); i++) { - Chunk &chunk = this->chunks_->at(i); + auto &chunk = this->chunks_->at(i); size_t start = i == 0 ? this->firstChunkOffset_ : 0; size_t end = @@ -160,7 +156,7 @@ public: { if (chunk->at(j) == item) { - Chunk newChunk = std::make_shared>(); + auto newChunk = std::make_shared(); newChunk->resize(chunk->size()); for (size_t k = 0; k < chunk->size(); k++) @@ -189,7 +185,7 @@ public: for (size_t i = 0; i < this->chunks_->size(); i++) { - Chunk &chunk = this->chunks_->at(i); + auto &chunk = this->chunks_->at(i); size_t start = i == 0 ? this->firstChunkOffset_ : 0; size_t end = @@ -199,7 +195,7 @@ public: { if (x == index) { - Chunk newChunk = std::make_shared>(); + auto newChunk = std::make_shared(); newChunk->resize(chunk->size()); for (size_t k = 0; k < chunk->size(); k++) @@ -233,7 +229,7 @@ private: qsizetype space() { size_t totalSize = 0; - for (Chunk &chunk : *this->chunks_) + for (auto &chunk : *this->chunks_) { totalSize += chunk->size(); } @@ -257,18 +253,15 @@ private: deleted = this->chunks_->front()->at(this->firstChunkOffset_); - this->firstChunkOffset_++; - // need to delete the first chunk if (this->firstChunkOffset_ == this->chunks_->front()->size() - 1) { // copy the chunk vector - ChunkVector newVector = std::make_shared< - std::vector>>>(); + auto newVector = std::make_shared(); // delete first chunk bool first = true; - for (Chunk &chunk : *this->chunks_) + for (auto &chunk : *this->chunks_) { if (!first) { @@ -280,15 +273,20 @@ private: this->chunks_ = newVector; this->firstChunkOffset_ = 0; } + else + { + this->firstChunkOffset_++; + } + return true; } - ChunkVector chunks_; + std::shared_ptr chunks_; std::mutex mutex_; size_t firstChunkOffset_; size_t lastChunkEnd_; - size_t limit_; + const size_t limit_; const size_t chunkSize_ = 100; }; diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index c8efc73d6..c187b5f55 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -12,7 +12,7 @@ namespace chatterino { class MessageElement; -enum class MessageFlag : uint16_t { +enum class MessageFlag : uint32_t { None = 0, System = (1 << 0), Timeout = (1 << 1), @@ -30,6 +30,7 @@ enum class MessageFlag : uint16_t { DoNotLog = (1 << 13), AutoMod = (1 << 14), RecentMessage = (1 << 15), + Whisper = (1 << 16) }; using MessageFlags = FlagsEnum; @@ -47,6 +48,7 @@ struct Message : boost::noncopyable { QTime parseTime; QString id; QString searchText; + QString messageText; QString loginName; QString displayName; QString localizedName; diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 2bc8e70dd..96a2d25c6 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -5,6 +5,7 @@ #include "messages/Image.hpp" #include "messages/Message.hpp" #include "messages/MessageElement.hpp" +#include "providers/LinkResolver.hpp" #include "providers/twitch/PubsubActions.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" @@ -64,6 +65,8 @@ std::pair makeAutomodMessage( builder = MessageBuilder(); builder.emplace(); + builder.emplace(); + builder.message().loginName = action.target.name; builder.message().flags.set(MessageFlag::PubSub); builder @@ -90,23 +93,30 @@ MessageBuilder::MessageBuilder() { } -MessageBuilder::MessageBuilder(const QString &text) - : MessageBuilder() -{ - this->emplace(); - this->emplace(text, MessageElementFlag::Text, - MessageColor::System); - this->message().searchText = text; -} - MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text) : MessageBuilder() { this->emplace(); - this->emplace(text, MessageElementFlag::Text, - MessageColor::System); + + // check system message for links + // (e.g. needed for sub ticket message in sub only mode) + const QStringList textFragments = text.split(QRegularExpression("\\s")); + for (const auto &word : textFragments) + { + const auto linkString = this->matchLink(word); + if (linkString.isEmpty()) + { + this->emplace(word, MessageElementFlag::Text, + MessageColor::System); + } + else + { + this->addLink(word, linkString); + } + } this->message().flags.set(MessageFlag::System); this->message().flags.set(MessageFlag::DoNotTriggerNotification); + this->message().messageText = text; this->message().searchText = text; } @@ -157,6 +167,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username, this->emplace(); this->emplace(text, MessageElementFlag::Text, MessageColor::System); + this->message().messageText = text; this->message().searchText = text; } @@ -213,6 +224,7 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count) this->emplace(text, MessageElementFlag::Text, MessageColor::System); + this->message().messageText = text; this->message().searchText = text; } @@ -225,23 +237,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action) 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); - } + QString text = + QString("%1 %2 %3.") + .arg(action.source.name) + .arg(QString(action.wasBan() ? "unbanned" : "untimedout")) + .arg(action.target.name); this->emplace(text, MessageElementFlag::Text, MessageColor::System); + this->message().messageText = text; this->message().searchText = text; } @@ -352,4 +356,57 @@ QString MessageBuilder::matchLink(const QString &string) return captured; } +void MessageBuilder::addLink(const QString &origLink, + const QString &matchedLink) +{ + static QRegularExpression domainRegex( + R"(^(?:(?:ftp|http)s?:\/\/)?([^\/]+)(?:\/.*)?$)", + QRegularExpression::CaseInsensitiveOption); + + QString lowercaseLinkString; + auto match = domainRegex.match(origLink); + if (match.isValid()) + { + lowercaseLinkString = origLink.mid(0, match.capturedStart(1)) + + match.captured(1).toLower() + + origLink.mid(match.capturedEnd(1)); + } + else + { + lowercaseLinkString = origLink; + } + auto linkElement = Link(Link::Url, matchedLink); + + auto textColor = MessageColor(MessageColor::Link); + auto linkMELowercase = + this->emplace(lowercaseLinkString, + MessageElementFlag::LowercaseLink, textColor) + ->setLink(linkElement); + auto linkMEOriginal = + this->emplace(origLink, MessageElementFlag::OriginalLink, + textColor) + ->setLink(linkElement); + + LinkResolver::getLinkInfo(matchedLink, [weakMessage = this->weakOf(), + linkMELowercase, linkMEOriginal, + matchedLink](QString tooltipText, + Link originalLink) { + auto shared = weakMessage.lock(); + if (!shared) + { + return; + } + if (!tooltipText.isEmpty()) + { + linkMELowercase->setTooltip(tooltipText); + linkMEOriginal->setTooltip(tooltipText); + } + if (originalLink.value != matchedLink && !originalLink.value.isEmpty()) + { + linkMELowercase->setLink(originalLink)->updateLink(); + linkMEOriginal->setLink(originalLink)->updateLink(); + } + }); +} + } // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 8fe7915e2..4793ce9d9 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -38,7 +38,6 @@ class MessageBuilder { public: MessageBuilder(); - MessageBuilder(const QString &text); MessageBuilder(SystemMessageTag, const QString &text); MessageBuilder(TimeoutMessageTag, const QString &username, const QString &durationInSeconds, const QString &reason, @@ -54,6 +53,7 @@ public: void append(std::unique_ptr element); QString matchLink(const QString &string); + void addLink(const QString &origLink, const QString &matchedLink); template T *emplace(Args &&... args) diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index 8afe72a5a..e5920fb6f 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -138,10 +138,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, if (image->isEmpty()) return; - auto emoteScale = - this->getFlags().hasAny(MessageElementFlag::Badges) - ? 1 - : getSettings()->emoteScale.getValue(); + auto emoteScale = getSettings()->emoteScale.getValue(); auto size = QSize(int(container.getScale() * image->width() * emoteScale), @@ -161,6 +158,31 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container, } } +// BADGE +BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags) + : MessageElement(flags) + , emote_(emote) +{ + this->setTooltip(emote->tooltip.string); +} + +void BadgeElement::addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags) +{ + if (flags.hasAny(this->getFlags())) + { + auto image = this->emote_->images.getImage(container.getScale()); + if (image->isEmpty()) + return; + + auto size = QSize(int(container.getScale() * image->width()), + int(container.getScale() * image->height())); + + container.addElement((new ImageLayoutElement(*this, image, size)) + ->setLink(this->getLink())); + } +} + // TEXT TextElement::TextElement(const QString &text, MessageElementFlags flags, const MessageColor &color, FontStyle style) @@ -188,7 +210,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, for (Word &word : this->words_) { auto getTextLayoutElement = [&](QString text, int width, - bool trailingSpace) { + bool hasTrailingSpace) { auto color = this->color_.getColor(*app->themes); app->themes->normalizeColor(color); @@ -196,7 +218,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container, *this, text, QSize(width, metrics.height()), color, this->style_, container.getScale())) ->setLink(this->getLink()); - e->setTrailingSpace(trailingSpace); + e->setTrailingSpace(hasTrailingSpace); e->setText(text); // If URL link was changed, @@ -291,7 +313,6 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container, { if (flags.hasAny(this->getFlags())) { - auto app = getApp(); if (getSettings()->timestampFormat != this->format_) { this->format_ = getSettings()->timestampFormat.getValue(); diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 4ae74d848..95c7090d4 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -42,6 +42,7 @@ enum class MessageElementFlag { FfzEmoteText = (1 << 11), FfzEmote = FfzEmoteImage | FfzEmoteText, EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, + EmoteText = TwitchEmoteText | BttvEmoteText | FfzEmoteText, BitsStatic = (1 << 12), BitsAnimated = (1 << 13), @@ -74,9 +75,6 @@ enum class MessageElementFlag { // - 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, @@ -216,6 +214,18 @@ private: EmotePtr emote_; }; +class BadgeElement : public MessageElement +{ +public: + BadgeElement(const EmotePtr &data, MessageElementFlags flags_); + + void addToContainer(MessageLayoutContainer &container, + MessageElementFlags flags_) override; + +private: + EmotePtr emote_; +}; + // contains a text, formated depending on the preferences class TimestampElement : public MessageElement { diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index e80a1b2c6..f1725f080 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -99,17 +99,14 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags) return true; } -void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags) +void MessageLayout::actuallyLayout(int width, MessageElementFlags flags) { this->layoutCount_++; - const auto addTest = this->message_->flags.hasAny( - {MessageFlag::DisconnectedMessage, MessageFlag::ConnectedMessage}); - auto messageFlags = this->message_->flags; if (this->flags.has(MessageLayoutFlag::Expanded) || - (_flags.has(MessageElementFlag::ModeratorTools) && + (flags.has(MessageElementFlag::ModeratorTools) && !this->message_->flags.has(MessageFlag::Disabled))) // { messageFlags.unset(MessageFlag::Collapsed); @@ -117,25 +114,21 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags) this->container_->begin(width, this->scale_, messageFlags); - if (addTest) - { - this->container_->addElementNoLineBreak(new TestLayoutElement( - EmptyElement::instance(), QSize(width, this->scale_ * 6), - getTheme()->messages.backgrounds.regular, false)); - this->container_->breakLine(); - } - for (const auto &element : this->message_->elements) { - element->addToContainer(*this->container_, _flags); - } + if (getSettings()->hideModerated && + this->message_->flags.has(MessageFlag::Disabled)) + { + continue; + } - if (addTest) - { - this->container_->breakLine(); - this->container_->addElement(new TestLayoutElement( - EmptyElement::instance(), QSize(width, this->scale_ * 6), - getTheme()->messages.backgrounds.regular, true)); + if (getSettings()->hideModerationActions && + this->message_->flags.has(MessageFlag::Timeout)) + { + continue; + } + + element->addToContainer(*this->container_, flags); } if (this->height_ != this->container_->getHeight()) @@ -200,29 +193,12 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, app->themes->messages.disabled); // painter.fillRect(0, y, pixmap->width(), pixmap->height(), // QBrush(QColor(64, 64, 64, 64))); - - if (getSettings()->redDisabledMessages) - { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), - QBrush(QColor(255, 0, 0, 63), Qt::BDiagPattern)); - // app->themes->messages.disabled); - } } if (this->message_->flags.has(MessageFlag::RecentMessage)) { - const auto &historicMessageAppearance = - getSettings()->historicMessagesAppearance.getValue(); - if (historicMessageAppearance & HistoricMessageAppearance::Crossed) - { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), - QBrush(QColor(255, 0, 0, 63), Qt::BDiagPattern)); - } - if (historicMessageAppearance & HistoricMessageAppearance::Greyed) - { - painter.fillRect(0, y, pixmap->width(), pixmap->height(), - app->themes->messages.disabled); - } + painter.fillRect(0, y, pixmap->width(), pixmap->height(), + app->themes->messages.disabled); } // draw selection diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index f24d05e26..599564787 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -157,7 +157,7 @@ void MessageLayoutContainer::breakLine() int yExtra = 0; if (isCompactEmote) { - // yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; + yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; } // if (element->getCreator().getFlags() & @@ -390,17 +390,17 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex, int lineIndex2 = lineIndex + 1; for (; lineIndex2 < this->lines_.size(); lineIndex2++) { - Line &line = this->lines_[lineIndex2]; - QRect rect = line.rect; + Line &line2 = this->lines_[lineIndex2]; + QRect rect = line2.rect; rect.setTop(std::max(0, rect.top()) + yOffset); rect.setBottom( std::min(this->height_, rect.bottom()) + yOffset); - rect.setLeft(this->elements_[line.startIndex] + rect.setLeft(this->elements_[line2.startIndex] ->getRect() .left()); - rect.setRight(this->elements_[line.endIndex - 1] + rect.setRight(this->elements_[line2.endIndex - 1] ->getRect() .right()); diff --git a/src/messages/layouts/MessageLayoutContainer.hpp b/src/messages/layouts/MessageLayoutContainer.hpp index e248038a3..112f2bb3c 100644 --- a/src/messages/layouts/MessageLayoutContainer.hpp +++ b/src/messages/layouts/MessageLayoutContainer.hpp @@ -14,7 +14,7 @@ class QPainter; namespace chatterino { -enum class MessageFlag : uint16_t; +enum class MessageFlag : uint32_t; using MessageFlags = FlagsEnum; struct Margin { diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp index 93e0afe93..a8bcc30c9 100644 --- a/src/messages/layouts/MessageLayoutElement.cpp +++ b/src/messages/layouts/MessageLayoutElement.cpp @@ -368,72 +368,4 @@ int TextIconLayoutElement::getXFromIndex(int index) } } -// TestLayoutElement -TestLayoutElement::TestLayoutElement(MessageElement &element, const QSize &size, - const QColor &background, bool end) - : MessageLayoutElement(element, size) - , size_(size) - , background_(background) - , end_(end) -{ -} - -void TestLayoutElement::addCopyTextToString(QString &str, int from, - int to) const -{ -} - -int TestLayoutElement::getSelectionIndexCount() const -{ - return 0; -} - -void TestLayoutElement::paint(QPainter &painter) -{ - const auto dy = this->getRect().y(); - const auto color = end_ ? background_ : QColor(0, 0, 0, 127); - - // make zig zag - auto polygon = QPolygon(); - for (auto x = size_.height() / -2; x < size_.width() + 16; - x += size_.height()) - { - polygon.push_back({x, dy + 0}); - polygon.push_back({x + size_.height(), dy + size_.height()}); - x += size_.height(); - polygon.push_back({x, dy + size_.height()}); - polygon.push_back({x + size_.height(), dy + 0}); - } - - // finish polygon - polygon.push_back({size_.width(), 1000}); - polygon.push_back({0, 1000}); - - // finish polygon - polygon.push_back({size_.width(), 1000}); - polygon.push_back({0, 1000}); - - // turn into path - auto path = QPainterPath(); - path.addPolygon(polygon); - - // draw - painter.fillPath(path, color); - painter.strokePath(path, QColor(127, 127, 127, 127)); -} - -void TestLayoutElement::paintAnimated(QPainter &painter, int yOffset) -{ -} - -int TestLayoutElement::getMouseOverIndex(const QPoint &abs) const -{ - return 0; -} - -int TestLayoutElement::getXFromIndex(int index) -{ - return 0; -} - } // namespace chatterino diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp index 24139be4e..a7214937a 100644 --- a/src/messages/layouts/MessageLayoutElement.hpp +++ b/src/messages/layouts/MessageLayoutElement.hpp @@ -41,6 +41,7 @@ public: virtual void paintAnimated(QPainter &painter, int yOffset) = 0; virtual int getMouseOverIndex(const QPoint &abs) const = 0; virtual int getXFromIndex(int index) = 0; + const Link &getLink() const; const QString &getText() const; FlagsEnum getFlags() const; @@ -125,25 +126,4 @@ private: QString line2; }; -class TestLayoutElement : public MessageLayoutElement -{ -public: - TestLayoutElement(MessageElement &creator, const QSize &size, - const QColor &background, bool end); - -protected: - void addCopyTextToString(QString &str, int from = 0, - int to = INT_MAX) const override; - int getSelectionIndexCount() const override; - void paint(QPainter &painter) override; - void paintAnimated(QPainter &painter, int yOffset) override; - int getMouseOverIndex(const QPoint &abs) const override; - int getXFromIndex(int index) override; - -private: - QSize size_; - QColor background_; - bool end_; -}; - } // namespace chatterino diff --git a/src/providers/LinkResolver.cpp b/src/providers/LinkResolver.cpp index d7cc3a1f5..0137a130c 100644 --- a/src/providers/LinkResolver.cpp +++ b/src/providers/LinkResolver.cpp @@ -1,6 +1,7 @@ #include "providers/LinkResolver.hpp" #include "common/Common.hpp" +#include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "messages/Link.hpp" #include "singletons/Settings.hpp" @@ -12,12 +13,15 @@ namespace chatterino { void LinkResolver::getLinkInfo( const QString url, std::function successCallback) { - QString requestUrl("https://braize.pajlada.com/chatterino/link_resolver/" + - QUrl::toPercentEncoding(url, "", "/:")); - + if (!getSettings()->linkInfoTooltip) + { + successCallback("No link info loaded", Link(Link::Url, url)); + return; + } // Uncomment to test crashes // QTimer::singleShot(3000, [=]() { - NetworkRequest request(requestUrl); + NetworkRequest request(Env::get().linkResolverUrl.arg( + QString::fromUtf8(QUrl::toPercentEncoding(url, "", "/:")))); request.setCaller(QThread::currentThread()); request.setTimeout(30000); request.onSuccess([successCallback, url](auto result) mutable -> Outcome { diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index d42f619ef..f510d697f 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -36,9 +36,9 @@ namespace { else { const auto &shortCodes = unparsedEmoji["short_names"]; - for (const auto &shortCode : shortCodes.GetArray()) + for (const auto &_shortCode : shortCodes.GetArray()) { - emojiData->shortCodes.emplace_back(shortCode.GetString()); + emojiData->shortCodes.emplace_back(_shortCode.GetString()); } } @@ -240,8 +240,6 @@ void Emojis::sortEmojis() void Emojis::loadEmojiSet() { - auto app = getApp(); - getSettings()->emojiSet.connect([=](const auto &emojiSet) { this->emojis.each([=](const auto &name, std::shared_ptr &emoji) { diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 7cb45b3c7..eb1d0def9 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -231,9 +231,8 @@ void AbstractIrcServer::onConnected() { std::lock_guard lock(this->channelMutex); - auto connected = makeSystemMessage("connected"); - connected->flags.set(MessageFlag::ConnectedMessage); - connected->flags.set(MessageFlag::Centered); + auto connectedMsg = makeSystemMessage("connected"); + connectedMsg->flags.set(MessageFlag::ConnectedMessage); auto reconnected = makeSystemMessage("reconnected"); reconnected->flags.set(MessageFlag::ConnectedMessage); @@ -257,7 +256,7 @@ void AbstractIrcServer::onConnected() continue; } - chan->addMessage(connected); + chan->addMessage(connectedMsg); } this->falloffCounter_ = 1; @@ -269,7 +268,7 @@ void AbstractIrcServer::onDisconnected() MessageBuilder b(systemMessage, "disconnected"); b->flags.set(MessageFlag::DisconnectedMessage); - auto disconnected = b.release(); + auto disconnectedMsg = b.release(); for (std::weak_ptr &weak : this->channels.values()) { @@ -279,7 +278,7 @@ void AbstractIrcServer::onDisconnected() continue; } - chan->addMessage(disconnected); + chan->addMessage(disconnectedMsg); } } diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 27df7cb8c..d6f091a76 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -20,12 +20,69 @@ namespace chatterino { +static QMap parseBadges(QString badgesString) +{ + QMap badges; + + for (auto badgeData : badgesString.split(',')) + { + auto parts = badgeData.split('/'); + if (parts.length() != 2) + { + continue; + } + + badges.insert(parts[0], parts[1]); + } + + return badges; +} + IrcMessageHandler &IrcMessageHandler::getInstance() { static IrcMessageHandler instance; return instance; } +std::vector IrcMessageHandler::parseMessage( + Channel *channel, Communi::IrcMessage *message) +{ + std::vector builtMessages; + + auto command = message->command(); + + if (command == "PRIVMSG") + { + return this->parsePrivMessage( + channel, static_cast(message)); + } + else if (command == "USERNOTICE") + { + return this->parseUserNoticeMessage(channel, message); + } + else if (command == "NOTICE") + { + return this->parseNoticeMessage( + static_cast(message)); + } + + return builtMessages; +} + +std::vector IrcMessageHandler::parsePrivMessage( + Channel *channel, Communi::IrcPrivateMessage *message) +{ + std::vector builtMessages; + MessageParseArgs args; + TwitchMessageBuilder builder(channel, message, args, message->content(), + message->isAction()); + if (!builder.isIgnored()) + { + builtMessages.emplace_back(builder.build()); + } + return builtMessages; +} + void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server) { @@ -203,28 +260,78 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message) // refresh all app->windows->repaintVisibleChatWidgets(chan.get()); + if (getSettings()->hideModerated) + { + app->windows->forceLayoutChannelViews(); + } +} + +void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message) +{ + // check parameter count + if (message->parameters().length() < 1) + { + return; + } + + QString chanName; + if (!trimChannelName(message->parameter(0), chanName)) + { + return; + } + + auto app = getApp(); + + // get channel + auto chan = app->twitch.server->getChannelOrEmpty(chanName); + + if (chan->isEmpty()) + { + log("[IrcMessageHandler:handleClearMessageMessage] Twitch channel {} " + "not " + "found", + chanName); + return; + } + + auto tags = message->tags(); + + QString targetID = tags.value("target-msg-id").toString(); + + chan->deleteMessage(targetID); } void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message) { - QVariant _mod = message->tag("mod"); + auto app = getApp(); + QString channelName; + if (!trimChannelName(message->parameter(0), channelName)) + { + return; + } + + auto c = app->twitch.server->getChannelOrEmpty(channelName); + if (c->isEmpty()) + { + return; + } + + QVariant _badges = message->tag("badges"); + if (_badges.isValid()) + { + TwitchChannel *tc = dynamic_cast(c.get()); + if (tc != nullptr) + { + auto parsedBadges = parseBadges(_badges.toString()); + tc->setVIP(parsedBadges.contains("vip")); + tc->setStaff(parsedBadges.contains("staff")); + } + } + + QVariant _mod = message->tag("mod"); if (_mod.isValid()) { - auto app = getApp(); - - QString channelName; - if (!trimChannelName(message->parameter(0), channelName)) - { - return; - } - - auto c = app->twitch.server->getChannelOrEmpty(channelName); - if (c->isEmpty()) - { - return; - } - TwitchChannel *tc = dynamic_cast(c.get()); if (tc != nullptr) { @@ -248,6 +355,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) if (!builder.isIgnored()) { + builder->flags.set(MessageFlag::Whisper); MessagePtr _message = builder.build(); app->twitch.server->lastUserThatWhisperedMe.set(builder.userName); @@ -273,6 +381,56 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message) } } +std::vector IrcMessageHandler::parseUserNoticeMessage( + Channel *channel, Communi::IrcMessage *message) +{ + std::vector builtMessages; + + auto data = message->toData(); + + auto tags = message->tags(); + auto parameters = message->parameters(); + + auto target = parameters[0]; + QString msgType = tags.value("msg-id", "").toString(); + QString content; + if (parameters.size() >= 2) + { + content = parameters[1]; + } + + if (msgType == "sub" || msgType == "resub" || msgType == "subgift") + { + // Sub-specific message. I think it's only allowed for "resub" messages + // atm + if (!content.isEmpty()) + { + MessageParseArgs args; + args.trimSubscriberUsername = true; + + TwitchMessageBuilder builder(channel, message, args, content, + false); + builder->flags.set(MessageFlag::Subscription); + builder->flags.unset(MessageFlag::Highlighted); + builtMessages.emplace_back(builder.build()); + } + } + + auto it = tags.find("system-msg"); + + if (it != tags.end()) + { + auto b = MessageBuilder(systemMessage, + parseTagString(it.value().toString())); + + b->flags.set(MessageFlag::Subscription); + auto newMessage = b.release(); + builtMessages.emplace_back(newMessage); + } + + return builtMessages; +} + void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server) { @@ -352,35 +510,60 @@ void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message) } } +std::vector IrcMessageHandler::parseNoticeMessage( + Communi::IrcNoticeMessage *message) +{ + std::vector builtMessages; + + builtMessages.emplace_back(makeSystemMessage(message->content())); + + return builtMessages; +} + void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) { auto app = getApp(); - MessagePtr msg = makeSystemMessage(message->content()); + auto builtMessages = this->parseNoticeMessage(message); - QString channelName; - if (!trimChannelName(message->target(), channelName)) + for (auto msg : builtMessages) { - // Notice wasn't targeted at a single channel, send to all twitch - // channels - app->twitch.server->forEachChannelAndSpecialChannels( - [msg](const auto &c) { - c->addMessage(msg); // - }); + QString channelName; + if (!trimChannelName(message->target(), channelName) || + channelName == "jtv") + { + // 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; + return; + } + + auto channel = app->twitch.server->getChannelOrEmpty(channelName); + + if (channel->isEmpty()) + { + log("[IrcManager:handleNoticeMessage] Channel {} not found in " + "channel " + "manager ", + channelName); + return; + } + + QString tags = message->tags().value("msg-id", "").toString(); + if (tags == "bad_delete_message_error" || tags == "usage_delete") + { + channel->addMessage(makeSystemMessage( + "Usage: \"/delete \" - can't take more " + "than one argument")); + } + else + { + channel->addMessage(msg); + } } - - auto channel = app->twitch.server->getChannelOrEmpty(channelName); - - if (channel->isEmpty()) - { - log("[IrcManager:handleNoticeMessage] Channel {} not found in channel " - "manager ", - channelName); - return; - } - - channel->addMessage(msg); } void IrcMessageHandler::handleWriteConnectionNoticeMessage( diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index 40f1efb12..861ee734e 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include "messages/Message.hpp" namespace chatterino { class TwitchServer; +class Channel; class IrcMessageHandler { @@ -13,17 +15,37 @@ class IrcMessageHandler public: static IrcMessageHandler &getInstance(); + // parseMessage parses a single IRC message into 0+ Chatterino messages + std::vector parseMessage(Channel *channel, + Communi::IrcMessage *message); + + // parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages + std::vector parsePrivMessage( + Channel *channel, Communi::IrcPrivateMessage *message); void handlePrivMessage(Communi::IrcPrivateMessage *message, TwitchServer &server); void handleRoomStateMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message); + void handleClearMessageMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message); void handleWhisperMessage(Communi::IrcMessage *message); + + // parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+ + // chatterino messages + std::vector parseUserNoticeMessage( + Channel *channel, Communi::IrcMessage *message); void handleUserNoticeMessage(Communi::IrcMessage *message, TwitchServer &server); + void handleModeMessage(Communi::IrcMessage *message); + + // parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino + // messages + std::vector parseNoticeMessage( + Communi::IrcNoticeMessage *message); void handleNoticeMessage(Communi::IrcNoticeMessage *message); + void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message); void handleJoinMessage(Communi::IrcMessage *message); diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 1d798f8ce..c397a2248 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -3,6 +3,7 @@ #include #include "Application.hpp" +#include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" #include "debug/Log.hpp" @@ -534,9 +535,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) return; } - NetworkRequest req( - "https://braize.pajlada.com/chatterino/twitchemotes/set/" + - emoteSet->key + "/"); + NetworkRequest req(Env::get().twitchEmoteSetResolverUrl.arg(emoteSet->key)); req.setUseQuickLoadCache(true); req.onError([](int errorCode) -> bool { diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index a42c344da..5b7a2c431 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -172,8 +172,6 @@ bool TwitchAccountManager::isLoggedIn() const bool TwitchAccountManager::removeUser(TwitchAccount *account) { - const auto &accs = this->accounts.getVector(); - auto userID(account->getUserId()); if (!userID.isEmpty()) { diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index a569de5ae..b8ce6509b 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/Common.hpp" +#include "common/Env.hpp" #include "common/NetworkRequest.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/notifications/NotificationController.hpp" @@ -9,6 +10,7 @@ #include "messages/Message.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/LoadBttvChannelEmote.hpp" +#include "providers/twitch/IrcMessageHandler.hpp" #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" @@ -29,10 +31,14 @@ namespace chatterino { namespace { + constexpr char MAGIC_MESSAGE_SUFFIX[] = u8" \U000E0000"; + + // parseRecentMessages takes a json object and returns a vector of + // Communi IrcMessages auto parseRecentMessages(const QJsonObject &jsonRoot, ChannelPtr channel) { QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); - std::vector messages; + std::vector messages; if (jsonMessages.empty()) return messages; @@ -40,26 +46,17 @@ namespace { 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); - - MessageParseArgs args; - TwitchMessageBuilder builder(channel.get(), privMsg, args); - builder.message().flags.set(MessageFlag::RecentMessage); - - if (!builder.isIgnored()) - messages.push_back(builder.build()); + messages.emplace_back( + Communi::IrcMessage::fromData(content, nullptr)); } return messages; } std::pair parseChatters(const QJsonObject &jsonRoot) { - static QStringList categories = {"moderators", "staff", "admins", - "global_mods", "viewers"}; + static QStringList categories = {"broadcaster", "vips", "moderators", + "staff", "admins", "global_mods", + "viewers"}; auto usernames = UsernameSet(); @@ -127,10 +124,6 @@ TwitchChannel::TwitchChannel(const QString &name, [=] { this->refreshLiveStatus(); }); this->liveStatusTimer_.start(60 * 1000); - // -- - this->messageSuffix_.append(' '); - this->messageSuffix_.append(QChar(0x206D)); - // debugging #if 0 for (int i = 0; i < 1000; i++) { @@ -199,13 +192,13 @@ void TwitchChannel::sendMessage(const QString &message) return; } - if (!this->hasModRights()) + if (!this->hasHighRateLimit()) { if (getSettings()->allowDuplicateMessages) { if (parsedMessage == this->lastSentMessage_) { - parsedMessage.append(this->messageSuffix_); + parsedMessage.append(MAGIC_MESSAGE_SUFFIX); } } } @@ -225,6 +218,16 @@ bool TwitchChannel::isMod() const return this->mod_; } +bool TwitchChannel::isVIP() const +{ + return this->vip_; +} + +bool TwitchChannel::isStaff() const +{ + return this->staff_; +} + void TwitchChannel::setMod(bool value) { if (this->mod_ != value) @@ -235,6 +238,26 @@ void TwitchChannel::setMod(bool value) } } +void TwitchChannel::setVIP(bool value) +{ + if (this->vip_ != value) + { + this->vip_ = value; + + this->userStateChanged.invoke(); + } +} + +void TwitchChannel::setStaff(bool value) +{ + if (this->staff_ != value) + { + this->staff_ = value; + + this->userStateChanged.invoke(); + } +} + bool TwitchChannel::isBroadcaster() const { auto app = getApp(); @@ -242,6 +265,11 @@ bool TwitchChannel::isBroadcaster() const return this->getName() == app->accounts->twitch.getCurrent()->getUserName(); } +bool TwitchChannel::hasHighRateLimit() const +{ + return this->isMod() || this->isBroadcaster() || this->isVIP(); +} + void TwitchChannel::addRecentChatter(const MessagePtr &message) { this->chatters_.access()->insert(message->displayName); @@ -445,7 +473,7 @@ void TwitchChannel::setLive(bool newLiveStatus) else { auto offline = - makeSystemMessage(this->getName() + " is offline"); + makeSystemMessage(this->getDisplayName() + " is offline"); this->addMessage(offline); } guard->live = newLiveStatus; @@ -575,12 +603,13 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document) void TwitchChannel::loadRecentMessages() { - static QString genericURL = - "https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + - getDefaultClientID(); + if (!getSettings()->loadTwitchMessageHistoryOnConnect) + { + return; + } - NetworkRequest request(genericURL.arg(this->roomId())); - request.makeAuthorizedV5(getDefaultClientID()); + NetworkRequest request( + Env::get().recentMessagesApiUrl.arg(this->getName())); request.setCaller(QThread::currentThread()); // can't be concurrent right now due to SignalVector // request.setExecuteConcurrently(true); @@ -592,7 +621,21 @@ void TwitchChannel::loadRecentMessages() auto messages = parseRecentMessages(result.parseJson(), shared); - shared->addMessagesAtStart(messages); + auto &handler = IrcMessageHandler::getInstance(); + + std::vector allBuiltMessages; + + for (auto message : messages) + { + for (auto builtMessage : + handler.parseMessage(shared.get(), message)) + { + builtMessage->flags.set(MessageFlag::RecentMessage); + allBuiltMessages.emplace_back(builtMessage); + } + } + + shared->addMessagesAtStart(allBuiltMessages); return Success; }); @@ -618,11 +661,11 @@ void TwitchChannel::refreshChatters() { // setting? const auto streamStatus = this->accessStreamStatus(); - + const auto viewerCount = static_cast(streamStatus->viewerCount); if (getSettings()->onlyFetchChattersForSmallerStreamers) { if (streamStatus->live && - streamStatus->viewerCount > getSettings()->smallStreamerLimit) + viewerCount > getSettings()->smallStreamerLimit) { return; } @@ -674,8 +717,8 @@ void TwitchChannel::refreshBadges() { auto &versions = (*badgeSets)[jsonBadgeSet.key()]; - auto _ = jsonBadgeSet->toObject()["versions"].toObject(); - for (auto jsonVersion_ = _.begin(); jsonVersion_ != _.end(); + auto _set = jsonBadgeSet->toObject()["versions"].toObject(); + for (auto jsonVersion_ = _set.begin(); jsonVersion_ != _set.end(); jsonVersion_++) { auto jsonVersion = jsonVersion_->toObject(); diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 0383cd43d..9c4089ff0 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -61,7 +61,10 @@ public: virtual bool canSendMessage() const override; virtual void sendMessage(const QString &message) override; virtual bool isMod() const override; + bool isVIP() const; + bool isStaff() const; virtual bool isBroadcaster() const override; + virtual bool hasHighRateLimit() const override; // Data const QString &subscriptionUrl(); @@ -123,6 +126,8 @@ private: void addPartedUser(const QString &user); void setLive(bool newLiveStatus); void setMod(bool value); + void setVIP(bool value); + void setStaff(bool value); void setRoomId(const QString &id); void setRoomModes(const RoomModes &roomModes_); @@ -151,6 +156,8 @@ private: FfzModBadge ffzCustomModBadge_; bool mod_ = false; + bool vip_ = false; + bool staff_ = false; UniqueAccess roomID_; UniqueAccess joinedUsers_; @@ -159,7 +166,6 @@ private: bool partedUsersMergeQueued_ = false; // -- - QByteArray messageSuffix_; QString lastSentMessage_; QObject lifetimeGuard_; QTimer liveStatusTimer_; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index b8c34cc65..b68eb8d8a 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -4,9 +4,9 @@ #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "controllers/ignores/IgnoreController.hpp" +#include "controllers/pings/PingController.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" -#include "providers/LinkResolver.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchChannel.hpp" @@ -80,6 +80,19 @@ bool TwitchMessageBuilder::isIgnored() const { if (sourceUserID == user.id) { + switch (static_cast( + getSettings()->showIgnoredUsersMessages.getValue())) + { + case ShowIgnoredUsersMessages::IfModerator: + if (this->channel->isMod() || + this->channel->isBroadcaster()) + return false; + break; + case ShowIgnoredUsersMessages::IfBroadcaster: + if (this->channel->isBroadcaster()) + return false; + break; + } log("Blocking message because it's from blocked user {}", user.name); return true; @@ -109,12 +122,24 @@ MessagePtr TwitchMessageBuilder::build() this->appendChannelName(); + if (this->tags.contains("rm-deleted")) + { + this->message().flags.set(MessageFlag::Disabled); + } + // timestamp bool isPastMsg = this->tags.contains("historical"); if (isPastMsg) { // This may be architecture dependent(datatype) - qint64 ts = this->tags.value("tmi-sent-ts").toLongLong(); + bool customReceived = false; + qint64 ts = + this->tags.value("rm-received-ts").toLongLong(&customReceived); + if (!customReceived) + { + ts = this->tags.value("tmi-sent-ts").toLongLong(); + } + QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(ts); this->emplace(dateTime.time()); } @@ -310,13 +335,13 @@ MessagePtr TwitchMessageBuilder::build() QRegularExpression emoteregex( "\\b" + std::get<2>(tup).string + "\\b", QRegularExpression::UseUnicodePropertiesOption); - auto match = emoteregex.match(midExtendedRef); - if (match.hasMatch()) + auto _match = emoteregex.match(midExtendedRef); + if (_match.hasMatch()) { - int last = match.lastCapturedIndex(); + int last = _match.lastCapturedIndex(); for (int i = 0; i <= last; ++i) { - std::get<0>(tup) = from + match.capturedStart(); + std::get<0>(tup) = from + _match.capturedStart(); twitchEmotes.push_back(std::move(tup)); } } @@ -414,7 +439,9 @@ MessagePtr TwitchMessageBuilder::build() this->addWords(splits, twitchEmotes); - this->message().searchText = this->userName + ": " + this->originalMessage_; + this->message().messageText = this->originalMessage_; + this->message().searchText = this->message().localizedName + " " + + this->userName + ": " + this->originalMessage_; return this->release(); } @@ -512,56 +539,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_) } 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); - auto linkMELowercase = - this->emplace(lowercaseLinkString, - MessageElementFlag::LowercaseLink, - textColor) - ->setLink(link); - auto linkMEOriginal = - this->emplace(string, MessageElementFlag::OriginalLink, - textColor) - ->setLink(link); - - LinkResolver::getLinkInfo( - linkString, - [weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal, - linkString](QString tooltipText, Link originalLink) { - auto shared = weakMessage.lock(); - if (!shared) - { - return; - } - if (!tooltipText.isEmpty()) - { - linkMELowercase->setTooltip(tooltipText); - linkMEOriginal->setTooltip(tooltipText); - } - if (originalLink.value != linkString && - !originalLink.value.isEmpty()) - { - linkMELowercase->setLink(originalLink)->updateLink(); - linkMEOriginal->setLink(originalLink)->updateLink(); - } - }); + this->addLink(string, linkString); } // if (!linkString.isEmpty()) { @@ -605,7 +583,7 @@ void TwitchMessageBuilder::parseMessageID() if (iterator != this->tags.end()) { - this->messageID = iterator.value().toString(); + this->message().id = iterator.value().toString(); } } @@ -632,7 +610,7 @@ void TwitchMessageBuilder::parseRoomID() void TwitchMessageBuilder::appendChannelName() { QString channelName("#" + this->channel->getName()); - Link link(Link::Url, this->channel->getName() + "\n" + this->messageID); + Link link(Link::Url, this->channel->getName() + "\n" + this->message().id); this->emplace(channelName, MessageElementFlag::ChannelName, MessageColor::System) // @@ -805,16 +783,10 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) } // update the media player url if necessary - QUrl highlightSoundUrl; - if (getSettings()->customHighlightSound) - { - highlightSoundUrl = - QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue()); - } - else - { - highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav"); - } + QUrl highlightSoundUrl = + getSettings()->customHighlightSound + ? QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue()) + : QUrl("qrc:/sounds/ping2.wav"); if (currentPlayerUrl != highlightSoundUrl) { @@ -916,13 +888,16 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg) if (!isPastMsg) { - if (playSound && - (!hasFocus || getSettings()->highlightAlwaysPlaySound)) + bool notMuted = !getApp()->pings->isMuted(this->channel->getName()); + bool resolveFocus = + !hasFocus || getSettings()->highlightAlwaysPlaySound; + + if (playSound && notMuted && resolveFocus) { player->play(); } - if (doAlert) + if (doAlert && notMuted) { getApp()->windows->sendAlert(); } @@ -983,52 +958,36 @@ void TwitchMessageBuilder::appendTwitchEmote( Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) { - // Special channels, like /whispers and /channels return here - // This means they will not render any BTTV or FFZ emotes - if (this->twitchChannel == nullptr) - { - auto *app = getApp(); - const auto &bttvemotes = app->twitch.server->getBttvEmotes(); - const auto &ffzemotes = app->twitch.server->getFfzEmotes(); - auto flags = MessageElementFlags(); - auto emote = boost::optional{}; - { // bttv/ffz emote - if ((emote = bttvemotes.emote(name))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = ffzemotes.emote(name))) - { - flags = MessageElementFlag::FfzEmote; - } - if (emote) - { - this->emplace(emote.get(), flags); - return Success; - } - } // bttv/ffz emote - return Failure; - } + auto *app = getApp(); + + const auto &globalBttvEmotes = app->twitch.server->getBttvEmotes(); + const auto &globalFfzEmotes = app->twitch.server->getFfzEmotes(); auto flags = MessageElementFlags(); auto emote = boost::optional{}; - if ((emote = this->twitchChannel->globalBttv().emote(name))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = this->twitchChannel->bttvEmote(name))) - { - flags = MessageElementFlag::BttvEmote; - } - else if ((emote = this->twitchChannel->globalFfz().emote(name))) + // Emote order: + // - FrankerFaceZ Channel + // - BetterTTV Channel + // - FrankerFaceZ Global + // - BetterTTV Global + if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) { flags = MessageElementFlag::FfzEmote; } - else if ((emote = this->twitchChannel->ffzEmote(name))) + else if (this->twitchChannel && + (emote = this->twitchChannel->bttvEmote(name))) + { + flags = MessageElementFlag::BttvEmote; + } + else if ((emote = globalFfzEmotes.emote(name))) { flags = MessageElementFlag::FfzEmote; } + else if ((emote = globalBttvEmotes.emote(name))) + { + flags = MessageElementFlag::BttvEmote; + } if (emote) { @@ -1064,11 +1023,11 @@ void TwitchMessageBuilder::appendTwitchBadges() try { if (twitchChannel) - if (const auto &badge = this->twitchChannel->twitchBadge( + if (const auto &_badge = this->twitchChannel->twitchBadge( "bits", cheerAmount)) { - this->emplace( - badge.get(), MessageElementFlag::BadgeVanity) + this->emplace( + _badge.get(), MessageElementFlag::BadgeVanity) ->setTooltip(tooltip); continue; } @@ -1079,10 +1038,10 @@ void TwitchMessageBuilder::appendTwitchBadges() } // Use default bit badge - if (auto badge = this->twitchChannel->globalTwitchBadges().badge( + if (auto _badge = this->twitchChannel->globalTwitchBadges().badge( "bits", cheerAmount)) { - this->emplace(badge.get(), + this->emplace(_badge.get(), MessageElementFlag::BadgeVanity) ->setTooltip(tooltip); } @@ -1112,7 +1071,7 @@ void TwitchMessageBuilder::appendTwitchBadges() { if (auto customModBadge = this->twitchChannel->ffzCustomModBadge()) { - this->emplace( + this->emplace( customModBadge.get(), MessageElementFlag::BadgeChannelAuthority) ->setTooltip((*customModBadge)->tooltip.string); @@ -1172,7 +1131,7 @@ void TwitchMessageBuilder::appendTwitchBadges() if (auto badgeEmote = this->twitchChannel->twitchBadge( "subscriber", badge.mid(11))) { - this->emplace( + this->emplace( badgeEmote.get(), MessageElementFlag::BadgeSubscription) ->setTooltip((*badgeEmote)->tooltip.string); continue; @@ -1193,17 +1152,17 @@ void TwitchMessageBuilder::appendTwitchBadges() if (auto badgeEmote = this->twitchChannel->twitchBadge(splits[0], splits[1])) { - this->emplace(badgeEmote.get(), + this->emplace(badgeEmote.get(), MessageElementFlag::BadgeVanity) ->setTooltip((*badgeEmote)->tooltip.string); continue; } - if (auto badge = this->twitchChannel->globalTwitchBadges().badge( + if (auto _badge = this->twitchChannel->globalTwitchBadges().badge( splits[0], splits[1])) { - this->emplace(badge.get(), + this->emplace(_badge.get(), MessageElementFlag::BadgeVanity) - ->setTooltip((*badge)->tooltip.string); + ->setTooltip((*_badge)->tooltip.string); continue; } } @@ -1216,7 +1175,7 @@ void TwitchMessageBuilder::appendChatterinoBadges() getApp()->chatterinoBadges->getBadge({this->userName}); if (chatterinoBadgePtr) { - this->emplace(*chatterinoBadgePtr, + this->emplace(*chatterinoBadgePtr, MessageElementFlag::BadgeChatterino); } } diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 5aa72b204..da7b3865c 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -41,7 +41,6 @@ public: MessageParseArgs args; const QVariantMap tags; - QString messageID; QString userName; bool isIgnored() const; @@ -55,8 +54,10 @@ private: void appendUsername(); void parseHighlights(bool isPastMsg); - void appendTwitchEmote(const QString &emote, - std::vector> &vec, std::vector &correctPositions); + void appendTwitchEmote( + const QString &emote, + std::vector> &vec, + std::vector &correctPositions); Outcome tryAppendEmote(const EmoteName &name); void addWords( diff --git a/src/providers/twitch/TwitchServer.cpp b/src/providers/twitch/TwitchServer.cpp index b12a46190..58f98494c 100644 --- a/src/providers/twitch/TwitchServer.cpp +++ b/src/providers/twitch/TwitchServer.cpp @@ -156,6 +156,10 @@ void TwitchServer::messageReceived(Communi::IrcMessage *message) { handler.handleClearChatMessage(message); } + else if (command == "CLEARMSG") + { + handler.handleClearMessageMessage(message); + } else if (command == "USERSTATE") { handler.handleUserStateMessage(message); @@ -219,7 +223,7 @@ std::shared_ptr TwitchServer::getCustomChannel( { static auto channel = std::make_shared("$$$", chatterino::Channel::Type::Misc); - static auto timer = [&] { + static auto getTimer = [&] { for (auto i = 0; i < 1000; i++) { channel->addMessage(makeSystemMessage(QString::number(i + 1))); @@ -264,7 +268,8 @@ std::shared_ptr TwitchServer::getChannelOrEmptyByID( if (!twitchChannel) continue; - if (twitchChannel->roomId() == channelId) + if (twitchChannel->roomId() == channelId && + twitchChannel->getName().splitRef(":").size() < 3) { return twitchChannel; } @@ -293,10 +298,11 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel, std::lock_guard guard(this->lastMessageMutex_); // std::queue - auto &lastMessage = channel->hasModRights() ? this->lastMessageMod_ - : this->lastMessagePleb_; - size_t maxMessageCount = channel->hasModRights() ? 99 : 19; - auto minMessageOffset = (channel->hasModRights() ? 100ms : 1100ms); + auto &lastMessage = channel->hasHighRateLimit() + ? this->lastMessageMod_ + : this->lastMessagePleb_; + size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19; + auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms); auto now = std::chrono::steady_clock::now(); diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index 3114cf2d6..0ba547135 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -37,7 +37,7 @@ bool Paths::isPortable() QString Paths::cacheDirectory() { - static QStringSetting cachePathSetting = [] { + static const auto pathSetting = [] { QStringSetting cachePathSetting("/cache/path"); cachePathSetting.connect([](const auto &newPath, auto) { @@ -47,9 +47,9 @@ QString Paths::cacheDirectory() return cachePathSetting; }(); - auto path = cachePathSetting.getValue(); + auto path = pathSetting.getValue(); - if (path == "") + if (path.isEmpty()) { return this->cacheDirectory_; } diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 953374193..602232371 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -4,7 +4,7 @@ #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" -#include "messages/HistoricMessageAppearance.hpp" +#include "singletons/Toasts.hpp" #include #include @@ -32,15 +32,15 @@ public: Qt::VerPattern}; QStringSetting lastMessageColor = {"/appearance/messages/lastMessageColor", ""}; - IntSetting historicMessagesAppearance = { - "/appearance/messages/historicMessagesAppearance", - HistoricMessageAppearance::Crossed | HistoricMessageAppearance::Greyed}; BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true}; BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", false}; BoolSetting separateMessages = {"/appearance/messages/separateMessages", false}; BoolSetting compactEmotes = {"/appearance/messages/compactEmotes", true}; + BoolSetting hideModerated = {"/appearance/messages/hideModerated", false}; + BoolSetting hideModerationActions = { + "/appearance/messages/hideModerationActions", false}; // BoolSetting collapseLongMessages = // {"/appearance/messages/collapseLongMessages", false}; @@ -48,7 +48,7 @@ public: "/appearance/messages/collapseMessagesMinLines", 0}; BoolSetting alternateMessages = { "/appearance/messages/alternateMessageBackground", false}; - IntSetting boldScale = {"/appearance/boldScale", 57}; + FloatSetting boldScale = {"/appearance/boldScale", 50}; BoolSetting showTabCloseButton = {"/appearance/showTabCloseButton", true}; BoolSetting showTabLive = {"/appearance/showTabLiveButton", false}; BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton", @@ -67,7 +67,6 @@ public: BoolSetting headerUptime = {"/appearance/splitheader/showUptime", false}; FloatSetting customThemeMultiplier = {"/appearance/customThemeMultiplier", -0.5f}; - BoolSetting redDisabledMessages = {"/appearance/redStripes", true}; // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", // false}; @@ -106,10 +105,7 @@ public: /// Emotes BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight", false}; - BoolSetting enableTwitchEmotes = {"/emotes/enableTwitchEmotes", true}; - BoolSetting enableBttvEmotes = {"/emotes/enableBTTVEmotes", true}; - BoolSetting enableFfzEmotes = {"/emotes/enableFFZEmotes", true}; - BoolSetting enableEmojis = {"/emotes/enableEmojis", true}; + BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true}; BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true}; FloatSetting emoteScale = {"/emotes/scale", 1.f}; @@ -128,6 +124,7 @@ public: /// Ingored Users BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", true}; + IntSetting showIgnoredUsersMessages = {"/ignore/showIgnoredUsers", 0}; /// Moderation QStringSetting timeoutAction = {"/moderation/timeoutAction", "Disable"}; @@ -147,7 +144,7 @@ public: "/highlighting/whisperHighlight/enableSound", false}; BoolSetting enableWhisperHighlightTaskbar = { "/highlighting/whisperHighlight/enableTaskbarFlashing", false}; - QStringSetting highlightColor = {"/highlighting/color", "#4B282C"}; + QStringSetting highlightColor = {"/highlighting/color", ""}; BoolSetting longAlerts = {"/highlighting/alerts", false}; @@ -175,6 +172,8 @@ public: "qrc:/sounds/ping3.wav"}; BoolSetting notificationToast = {"/notifications/enableToast", false}; + IntSetting openFromToast = {"/notifications/openFromToast", + static_cast(ToastReaction::OpenInBrowser)}; /// External tools // Streamlink @@ -188,6 +187,9 @@ public: /// Misc IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; + BoolSetting loadTwitchMessageHistoryOnConnect = { + "/misc/twitch/loadMessageHistoryOnConnect", true}; + IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 0}; QStringSetting cachePath = {"/cache/path", ""}; diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 70d70776c..b99b42a5d 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -27,9 +27,9 @@ void Theme::actuallyUpdate(double hue, double multiplier) return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); }; - auto sat = qreal(0); - auto isLight_ = this->isLightTheme(); - auto flat = isLight_; + const auto sat = qreal(0); + const auto isLight = this->isLightTheme(); + const auto flat = isLight; if (this->isLightTheme()) { @@ -38,6 +38,9 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.resizeHandle = QColor(0, 148, 255, 0xff); this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50); + + // Highlighted Messages: theme support quick-fix + this->messages.backgrounds.highlighted = QColor("#BD8489"); } else { @@ -46,13 +49,16 @@ void Theme::actuallyUpdate(double hue, double multiplier) this->splits.resizeHandle = QColor(0, 148, 255, 0x70); this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20); + + // Highlighted Messages: theme support quick-fix + this->messages.backgrounds.highlighted = QColor("#4B282C"); } 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"); + 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); @@ -62,20 +68,25 @@ void Theme::actuallyUpdate(double hue, double multiplier) "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()); this->splits.input.focusedLine = this->tabs.highlighted.line.regular; this->splits.messageSeperator = - isLight_ ? QColor(127, 127, 127) : QColor(60, 60, 60); + 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); // Highlighted Messages - this->messages.backgrounds.highlighted = - QColor(getSettings()->highlightColor); + // hidden setting from PR #744 - if set it will overwrite theme color + // TODO: implement full theme support + if (getSettings()->highlightColor != "") + { + this->messages.backgrounds.highlighted = + QColor(getSettings()->highlightColor); + } } void Theme::normalizeColor(QColor &color) diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index 367724368..6b5ba9636 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -8,6 +8,8 @@ #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Paths.hpp" +#include "util/StreamLink.hpp" +#include "widgets/helper/CommonTexts.hpp" #ifdef Q_OS_WIN @@ -25,13 +27,40 @@ namespace chatterino { +std::map Toasts::reactionToString = { + {ToastReaction::OpenInBrowser, OPEN_IN_BROWSER}, + {ToastReaction::OpenInPlayer, OPEN_PLAYER_IN_BROWSER}, + {ToastReaction::OpenInStreamlink, OPEN_IN_STREAMLINK}, + {ToastReaction::DontOpen, DONT_OPEN}}; + bool Toasts::isEnabled() { #ifdef Q_OS_WIN return WinToastLib::WinToast::isCompatible() && getSettings()->notificationToast; -#endif +#else return false; +#endif +} + +QString Toasts::findStringFromReaction(const ToastReaction &reaction) +{ + auto iterator = Toasts::reactionToString.find(reaction); + if (iterator != Toasts::reactionToString.end()) + { + return iterator->second; + } + else + { + return DONT_OPEN; + } +} + +QString Toasts::findStringFromReaction( + const pajlada::Settings::Setting &value) +{ + int i = static_cast(value); + return Toasts::findStringFromReaction(static_cast(i)); } void Toasts::sendChannelNotification(const QString &channelName, Platform p) @@ -86,11 +115,33 @@ public: void toastActivated() const { QString link; - if (platform_ == Platform::Twitch) + auto toastReaction = + static_cast(getSettings()->openFromToast.getValue()); + + switch (toastReaction) { - link = "http://www.twitch.tv/" + channelName_; + case ToastReaction::OpenInBrowser: + if (platform_ == Platform::Twitch) + { + link = "http://www.twitch.tv/" + channelName_; + } + QDesktopServices::openUrl(QUrl(link)); + break; + case ToastReaction::OpenInPlayer: + if (platform_ == Platform::Twitch) + { + link = "https://player.twitch.tv/?channel=" + channelName_; + } + QDesktopServices::openUrl(QUrl(link)); + break; + case ToastReaction::OpenInStreamlink: + { + openStreamlinkForChannel(channelName_); + break; + } + // the fourth and last option is "don't open" + // in this case obviously nothing should happen } - QDesktopServices::openUrl(QUrl(link)); } void toastActivated(int actionIndex) const @@ -115,8 +166,17 @@ void Toasts::sendWindowsNotification(const QString &channelName, Platform p) std::wstring widestr = std::wstring(utf8_text.begin(), utf8_text.end()); templ.setTextField(widestr, WinToastLib::WinToastTemplate::FirstLine); - templ.setTextField(L"Click here to open in browser", - WinToastLib::WinToastTemplate::SecondLine); + if (static_cast(getSettings()->openFromToast.getValue()) != + ToastReaction::DontOpen) + { + QString mode = + Toasts::findStringFromReaction(getSettings()->openFromToast); + mode = mode.toLower(); + + templ.setTextField(L"Click here to " + mode.toStdWString(), + WinToastLib::WinToastTemplate::SecondLine); + } + QString Path; if (p == Platform::Twitch) { diff --git a/src/singletons/Toasts.hpp b/src/singletons/Toasts.hpp index 3a245346f..43c6d1da8 100644 --- a/src/singletons/Toasts.hpp +++ b/src/singletons/Toasts.hpp @@ -7,10 +7,21 @@ namespace chatterino { enum class Platform : uint8_t; +enum class ToastReaction { + OpenInBrowser = 0, + OpenInPlayer = 1, + OpenInStreamlink = 2, + DontOpen = 3 +}; + class Toasts final : public Singleton { public: void sendChannelNotification(const QString &channelName, Platform p); + static QString findStringFromReaction(const ToastReaction &reaction); + static QString findStringFromReaction( + const pajlada::Settings::Setting &reaction); + static std::map reactionToString; static bool isEnabled(); @@ -18,6 +29,7 @@ private: #ifdef Q_OS_WIN void sendWindowsNotification(const QString &channelName, Platform p); #endif + static void fetchChannelAvatar( const QString channelName, std::function successCallback); diff --git a/src/singletons/TooltipPreviewImage.cpp b/src/singletons/TooltipPreviewImage.cpp new file mode 100644 index 000000000..99cd9548b --- /dev/null +++ b/src/singletons/TooltipPreviewImage.cpp @@ -0,0 +1,37 @@ +#include "TooltipPreviewImage.hpp" + +#include "Application.hpp" +#include "singletons/WindowManager.hpp" +#include "widgets/TooltipWidget.hpp" + +namespace chatterino { +TooltipPreviewImage &TooltipPreviewImage::getInstance() +{ + static TooltipPreviewImage *instance = new TooltipPreviewImage(); + return *instance; +} + +TooltipPreviewImage::TooltipPreviewImage() +{ + connections_.push_back(getApp()->windows->gifRepaintRequested.connect([&] { + auto tooltipWidget = TooltipWidget::getInstance(); + if (this->image_ && !tooltipWidget->isHidden()) + { + auto pixmap = this->image_->pixmap(); + if (pixmap) + { + tooltipWidget->setImage(*pixmap); + } + } + else + { + tooltipWidget->clearImage(); + } + })); +} + +void TooltipPreviewImage::setImage(ImagePtr image) +{ + this->image_ = image; +} +} // namespace chatterino diff --git a/src/singletons/TooltipPreviewImage.hpp b/src/singletons/TooltipPreviewImage.hpp new file mode 100644 index 000000000..67ba9cd95 --- /dev/null +++ b/src/singletons/TooltipPreviewImage.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "messages/Image.hpp" + +namespace chatterino { +class TooltipPreviewImage +{ +public: + static TooltipPreviewImage &getInstance(); + void setImage(ImagePtr image); + + TooltipPreviewImage(const TooltipPreviewImage &) = delete; + +private: + TooltipPreviewImage(); + +private: + ImagePtr image_ = nullptr; + std::vector connections_; +}; +} // namespace chatterino diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index ca797be17..59c84c7e3 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -75,10 +76,7 @@ WindowManager::WindowManager() this->wordFlagsListener_.addSetting(settings->showBadgesSubscription); this->wordFlagsListener_.addSetting(settings->showBadgesVanity); this->wordFlagsListener_.addSetting(settings->showBadgesChatterino); - this->wordFlagsListener_.addSetting(settings->enableBttvEmotes); - this->wordFlagsListener_.addSetting(settings->enableEmojis); - this->wordFlagsListener_.addSetting(settings->enableFfzEmotes); - this->wordFlagsListener_.addSetting(settings->enableTwitchEmotes); + this->wordFlagsListener_.addSetting(settings->enableEmoteImages); this->wordFlagsListener_.addSetting(settings->boldUsernames); this->wordFlagsListener_.addSetting(settings->lowercaseDomains); this->wordFlagsListener_.setCB([this] { @@ -114,13 +112,12 @@ void WindowManager::updateWordTypeMask() } // emotes - 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); + if (settings->enableEmoteImages) + { + flags.set(MEF::EmoteImages); + } + flags.set(MEF::EmoteText); + flags.set(MEF::EmojiText); // bits flags.set(MEF::BitsAmount); @@ -258,11 +255,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths) // load file QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; - QFile file(settingsPath); - file.open(QIODevice::ReadOnly); - QByteArray data = file.readAll(); - QJsonDocument document = QJsonDocument::fromJson(data); - QJsonArray windows_arr = document.object().value("windows").toArray(); + QJsonArray windows_arr = this->loadWindowArray(settingsPath); // "deserialize" for (QJsonValue window_val : windows_arr) @@ -390,10 +383,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths) void WindowManager::save() { log("[WindowManager] Saving"); - assertInGuiThread(); - auto app = getApp(); - QJsonDocument document; // "serialize" @@ -477,7 +467,7 @@ void WindowManager::save() // save file QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; - QFile file(settingsPath); + QSaveFile file(settingsPath); file.open(QIODevice::WriteOnly | QIODevice::Truncate); QJsonDocument::JsonFormat format = @@ -489,7 +479,7 @@ void WindowManager::save() ; file.write(document.toJson(format)); - file.flush(); + file.commit(); } void WindowManager::sendAlert() @@ -516,6 +506,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj) case SplitNode::_Split: { obj.insert("type", "split"); + obj.insert("moderationMode", node->getSplit()->getModerationMode()); QJsonObject split; encodeChannel(node->getSplit()->getIndirectChannel(), split); obj.insert("data", split); @@ -621,4 +612,14 @@ void WindowManager::incGeneration() this->generation_++; } +QJsonArray WindowManager::loadWindowArray(const QString &settingsPath) +{ + QFile file(settingsPath); + file.open(QIODevice::ReadOnly); + QByteArray data = file.readAll(); + QJsonDocument document = QJsonDocument::fromJson(data); + QJsonArray windows_arr = document.object().value("windows").toArray(); + return windows_arr; +} + } // namespace chatterino diff --git a/src/singletons/WindowManager.hpp b/src/singletons/WindowManager.hpp index 038eb41ab..dd9e7fb69 100644 --- a/src/singletons/WindowManager.hpp +++ b/src/singletons/WindowManager.hpp @@ -54,6 +54,7 @@ public: virtual void initialize(Settings &settings, Paths &paths) override; virtual void save() override; void closeAll(); + QJsonArray loadWindowArray(const QString &settingsPath); int getGeneration() const; void incGeneration(); diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index 74b4ebe30..82230edcc 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -33,20 +33,9 @@ LoggingChannel::LoggingChannel(const QString &_channelName) // FOURTF: change this when adding more providers this->subDirectory = "Twitch/" + this->subDirectory; - auto app = getApp(); - getSettings()->logPath.connect([this](const QString &logPath, auto) { - auto app = getApp(); - - if (logPath.isEmpty()) - { - this->baseDirectory = getPaths()->messageLogDirectory; - } - else - { - this->baseDirectory = logPath; - } - + this->baseDirectory = + logPath.isEmpty() ? getPaths()->messageLogDirectory : logPath; this->openLogFile(); }); } diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index c213c83d8..4c3634c2f 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -36,8 +36,6 @@ namespace { QString getStreamlinkProgram() { - auto app = getApp(); - if (getSettings()->streamlinkUseCustomPath) { return getSettings()->streamlinkPath + "/" + getBinaryName(); @@ -66,7 +64,6 @@ namespace { { static QErrorMessage *msg = new QErrorMessage; - auto app = getApp(); if (getSettings()->streamlinkUseCustomPath) { msg->showMessage( @@ -172,8 +169,6 @@ void getStreamQualities(const QString &channelURL, void openStreamlink(const QString &channelURL, const QString &quality, QStringList extraArguments) { - auto app = getApp(); - QStringList arguments; QString additionalOptions = getSettings()->streamlinkOpts.getValue(); @@ -202,8 +197,6 @@ void openStreamlink(const QString &channelURL, const QString &quality, void openStreamlinkForChannel(const QString &channel) { - auto app = getApp(); - QString channelURL = "twitch.tv/" + channel; QString preferredQuality = getSettings()->preferredQuality; diff --git a/src/widgets/AccountSwitchPopupWidget.cpp b/src/widgets/AccountSwitchPopupWidget.cpp index 6e9cfcebe..693fe6da0 100644 --- a/src/widgets/AccountSwitchPopupWidget.cpp +++ b/src/widgets/AccountSwitchPopupWidget.cpp @@ -14,6 +14,9 @@ AccountSwitchPopupWidget::AccountSwitchPopupWidget(QWidget *parent) : QWidget(parent) { this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); +#ifdef Q_OS_LINUX + this->setWindowFlag(Qt::Popup); +#endif this->setContentsMargins(0, 0, 0, 0); diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index 88a165afb..416834f70 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -6,11 +6,11 @@ #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" #include "util/InitUpdateButton.hpp" +#include "util/Shortcut.hpp" #include "widgets/Window.hpp" #include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/helper/NotebookButton.hpp" #include "widgets/helper/NotebookTab.hpp" -#include "util/Shortcut.hpp" #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index a202dcd5c..4c7efeede 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -110,7 +110,7 @@ void Scrollbar::setSmallChange(qreal value) void Scrollbar::setDesiredValue(qreal value, bool animated) { - animated &= getSettings()->enableSmoothScrolling.getValue(); + animated &= getSettings()->enableSmoothScrolling; value = std::max(this->minimum_, std::min(this->maximum_ - this->largeChange_, value)); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index fb5551522..11741e015 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -29,6 +29,7 @@ #include #include +#include #include namespace chatterino { @@ -43,6 +44,10 @@ Window::Window(WindowType type) this->addShortcuts(); this->addLayout(); +#ifdef Q_OS_MACOS + this->addMenuBar(); +#endif + this->signalHolder_.managedConnect( getApp()->accounts->twitch.currentUserChanged, [this] { this->onAccountSelected(); }); @@ -336,6 +341,18 @@ void Window::addShortcuts() }); } +void Window::addMenuBar() +{ + QMenuBar *mainMenu = new QMenuBar(); + mainMenu->setNativeMenuBar(true); + + QMenu *menu = new QMenu(QString()); + mainMenu->addMenu(menu); + QAction *prefs = menu->addAction(QString()); + prefs->setMenuRole(QAction::PreferencesRole); + connect(prefs, &QAction::triggered, this, [] { SettingsDialog::showDialog(); }); +} + #define UGLYMACROHACK1(s) #s #define UGLYMACROHACK(s) UGLYMACROHACK1(s) diff --git a/src/widgets/Window.hpp b/src/widgets/Window.hpp index e27bdfae9..d2c1c1922 100644 --- a/src/widgets/Window.hpp +++ b/src/widgets/Window.hpp @@ -38,6 +38,7 @@ private: void addShortcuts(); void addLayout(); void onAccountSelected(); + void addMenuBar(); WindowType type_; diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index 2d8218e69..f965ccb5c 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -319,7 +319,6 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched, { return false; } - return true; } else if (event->type() == QEvent::KeyRelease) { diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 10ad52fc8..f326d9cc9 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -84,6 +84,8 @@ UserInfoPopup::UserInfoPopup() .assign(&this->ui_.ignoreHighlights); auto viewLogs = user.emplace(this); viewLogs->getLabel().setText("Online logs"); + auto usercard = user.emplace(this); + usercard->getLabel().setText("Usercard"); auto mod = user.emplace