Merge pull request #1133 from Chatterino/nightly

Merge Nightly into Master
This commit is contained in:
pajlada 2019-07-13 12:21:12 +02:00 committed by GitHub
commit 6662053061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 2549 additions and 1166 deletions

View file

@ -0,0 +1,3 @@
# Description
<!-- If applicable, please include a summary of what you've changed and what issue is fixed. In the case of a bug fix, please include steps to reproduce the bug so the pull request can be tested -->

2
.gitmodules vendored
View file

@ -17,4 +17,4 @@
[submodule "lib/appbase"] [submodule "lib/appbase"]
path = lib/appbase path = lib/appbase
url = https://github.com/fourtf/appbase url = https://github.com/Chatterino/appbase

View file

@ -1,10 +1,35 @@
before_install: os: osx
- sudo add-apt-repository --yes ppa:ubuntu-sdk-team/ppa osx_image: xcode10.2
- sudo apt-get update -qq
- sudo apt-get install qtbase5-dev qtdeclarative5-dev libqt5webkit5-dev libsqlite3-dev addons:
- sudo apt-get install qt5-default qttools5-dev-tools homebrew:
packages:
- boost
- openssl
- rapidjson
- qt
- p7zip
compiler: clang
script: script:
- qmake -qt=qt5 -v - mkdir build && cd build
- qmake -qt=qt5 - /usr/local/opt/qt/bin/qmake .. && make -j8
- make - /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

View file

@ -4,7 +4,7 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
## Ubuntu 18.04 ## Ubuntu 18.04
*most likely works the same for other Debian-like distros* *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 1. Open `chatterino.pro` with QT Creator and build
## Arch Linux ## Arch Linux

View file

@ -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 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 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)). 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)).

37
CMakeLists.txt Normal file
View file

@ -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()

View file

@ -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` 7. Under `Clang Format` select `Use predefined style: File` and `Fallback style: None`
Qt creator should now format the documents when saving it. Qt creator should now format the documents when saving it.

65
appveyor.yml Normal file
View file

@ -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

View file

@ -1,403 +1,424 @@
#------------------------------------------------- QT += widgets core gui network multimedia svg concurrent
# CONFIG += communi
# Project created by QtCreator 2016-12-28T18:23:35 COMMUNI += core model util
#
#------------------------------------------------- INCLUDEPATH += src/
TARGET = chatterino
message(----) TEMPLATE = app
PRECOMPILED_HEADER = src/PrecompiledHeader.hpp
# define project shit CONFIG += precompile_header
QT += widgets core gui network multimedia svg concurrent DEFINES += CHATTERINO
CONFIG += communi DEFINES += "AB_NAMESPACE=chatterino"
COMMUNI += core model util DEFINES += AB_CUSTOM_THEME
DEFINES += AB_CUSTOM_SETTINGS
INCLUDEPATH += src/ CONFIG += AB_NOT_STANDALONE
TARGET = chatterino
TEMPLATE = app useBreakpad {
PRECOMPILED_HEADER = src/PrecompiledHeader.hpp LIBS += -L$$PWD/lib/qBreakpad/handler/build
CONFIG += precompile_header include(lib/qBreakpad/qBreakpad.pri)
DEFINES += CHATTERINO DEFINES += C_USE_BREAKPAD
DEFINES += "AB_NAMESPACE=chatterino" }
DEFINES += AB_CUSTOM_THEME
DEFINES += AB_CUSTOM_SETTINGS # https://bugreports.qt.io/browse/QTBUG-27018
CONFIG += AB_NOT_STANDALONE equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") {
TARGET = bin/chatterino
useBreakpad { }
LIBS += -L$$PWD/lib/qBreakpad/handler/build
include(lib/qBreakpad/qBreakpad.pri) # Icons
DEFINES += C_USE_BREAKPAD macx:ICON = resources/chatterino.icns
} win32:RC_FILE = resources/windows.rc
# https://bugreports.qt.io/browse/QTBUG-27018 macx {
equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { LIBS += -L/usr/local/lib
TARGET = bin/chatterino }
}
# Submodules
# Icons include(lib/appbase.pri)
#macx:ICON = resources/images/chatterino2.icns include(lib/humanize.pri)
win32:RC_FILE = resources/windows.rc DEFINES += IRC_NAMESPACE=Communi
include(lib/libcommuni.pri)
macx { include(lib/websocketpp.pri)
LIBS += -L/usr/local/lib include(lib/openssl.pri)
} include(lib/wintoast.pri)
# Submodules # Optional feature: QtWebEngine
include(lib/appbase.pri) #exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) {
include(lib/humanize.pri) # message(Using QWebEngine)
DEFINES += IRC_NAMESPACE=Communi # QT += webenginewidgets
include(lib/libcommuni.pri) # DEFINES += "USEWEBENGINE"
include(lib/websocketpp.pri) #}
include(lib/openssl.pri)
include(lib/wintoast.pri) SOURCES += \
src/Application.cpp \
# Optional feature: QtWebEngine src/autogenerated/ResourcesAutogen.cpp \
#exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { src/BrowserExtension.cpp \
# message(Using QWebEngine) src/common/Channel.cpp \
# QT += webenginewidgets src/common/CompletionModel.cpp \
# DEFINES += "USEWEBENGINE" src/common/DownloadManager.cpp \
#} src/common/Env.cpp \
src/common/LinkParser.cpp \
SOURCES += \ src/common/NetworkData.cpp \
src/Application.cpp \ src/common/NetworkManager.cpp \
src/common/Channel.cpp \ src/common/NetworkRequest.cpp \
src/common/CompletionModel.cpp \ src/common/NetworkResult.cpp \
src/common/NetworkData.cpp \ src/common/NetworkTimer.cpp \
src/common/NetworkManager.cpp \ src/common/UsernameSet.cpp \
src/common/NetworkRequest.cpp \ src/controllers/accounts/Account.cpp \
src/common/NetworkResult.cpp \ src/controllers/accounts/AccountController.cpp \
src/common/NetworkTimer.cpp \ src/controllers/accounts/AccountModel.cpp \
src/controllers/accounts/Account.cpp \ src/controllers/commands/Command.cpp \
src/controllers/accounts/AccountController.cpp \ src/controllers/commands/CommandController.cpp \
src/controllers/accounts/AccountModel.cpp \ src/controllers/commands/CommandModel.cpp \
src/controllers/commands/Command.cpp \ src/controllers/highlights/HighlightBlacklistModel.cpp \
src/controllers/commands/CommandController.cpp \ src/controllers/highlights/HighlightController.cpp \
src/controllers/commands/CommandModel.cpp \ src/controllers/highlights/HighlightModel.cpp \
src/controllers/highlights/HighlightController.cpp \ src/controllers/highlights/UserHighlightModel.cpp \
src/controllers/highlights/HighlightModel.cpp \ src/controllers/ignores/IgnoreController.cpp \
src/controllers/highlights/HighlightBlacklistModel.cpp \ src/controllers/ignores/IgnoreModel.cpp \
src/controllers/highlights/UserHighlightModel.cpp \ src/controllers/moderationactions/ModerationAction.cpp \
src/controllers/ignores/IgnoreController.cpp \ src/controllers/moderationactions/ModerationActionModel.cpp \
src/controllers/ignores/IgnoreModel.cpp \ src/controllers/moderationactions/ModerationActions.cpp \
src/controllers/notifications/NotificationController.cpp \ src/controllers/notifications/NotificationController.cpp \
src/controllers/taggedusers/TaggedUser.cpp \ src/controllers/notifications/NotificationModel.cpp \
src/controllers/taggedusers/TaggedUsersController.cpp \ src/controllers/taggedusers/TaggedUser.cpp \
src/controllers/taggedusers/TaggedUsersModel.cpp \ src/controllers/taggedusers/TaggedUsersController.cpp \
src/main.cpp \ src/controllers/taggedusers/TaggedUsersModel.cpp \
src/messages/Image.cpp \ src/main.cpp \
src/messages/layouts/MessageLayout.cpp \ src/messages/Emote.cpp \
src/messages/layouts/MessageLayoutContainer.cpp \ src/messages/Image.cpp \
src/messages/layouts/MessageLayoutElement.cpp \ src/messages/ImageSet.cpp \
src/messages/Link.cpp \ src/messages/layouts/MessageLayout.cpp \
src/messages/Message.cpp \ src/messages/layouts/MessageLayoutContainer.cpp \
src/messages/MessageBuilder.cpp \ src/messages/layouts/MessageLayoutElement.cpp \
src/messages/MessageColor.cpp \ src/messages/Link.cpp \
src/messages/MessageElement.cpp \ src/messages/Message.cpp \
src/providers/emoji/Emojis.cpp \ src/messages/MessageBuilder.cpp \
src/providers/irc/AbstractIrcServer.cpp \ src/messages/MessageColor.cpp \
src/providers/irc/IrcAccount.cpp \ src/messages/MessageContainer.cpp \
src/providers/irc/IrcChannel2.cpp \ src/messages/MessageElement.cpp \
src/providers/irc/IrcConnection2.cpp \ src/providers/bttv/BttvEmotes.cpp \
src/providers/irc/IrcServer.cpp \ src/providers/bttv/LoadBttvChannelEmote.cpp \
src/providers/twitch/IrcMessageHandler.cpp \ src/providers/chatterino/ChatterinoBadges.cpp \
src/providers/twitch/PartialTwitchUser.cpp \ src/providers/emoji/Emojis.cpp \
src/providers/twitch/PubsubActions.cpp \ src/providers/ffz/FfzEmotes.cpp \
src/providers/twitch/PubsubHelpers.cpp \ src/providers/ffz/FfzModBadge.cpp \
src/providers/twitch/TwitchAccount.cpp \ src/providers/irc/AbstractIrcServer.cpp \
src/providers/twitch/TwitchAccountManager.cpp \ src/providers/irc/IrcAccount.cpp \
src/providers/twitch/TwitchChannel.cpp \ src/providers/irc/IrcChannel2.cpp \
src/providers/twitch/TwitchEmotes.cpp \ src/providers/irc/IrcConnection2.cpp \
src/providers/twitch/TwitchHelpers.cpp \ src/providers/irc/IrcServer.cpp \
src/providers/twitch/TwitchMessageBuilder.cpp \ src/providers/LinkResolver.cpp \
src/providers/twitch/TwitchServer.cpp \ src/providers/twitch/ChatroomChannel.cpp \
src/providers/twitch/TwitchUser.cpp \ src/providers/twitch/IrcMessageHandler.cpp \
src/singletons/helper/GifTimer.cpp \ src/providers/twitch/PartialTwitchUser.cpp \
src/singletons/helper/LoggingChannel.cpp \ src/providers/twitch/PubsubActions.cpp \
src/controllers/moderationactions/ModerationAction.cpp \ src/providers/twitch/PubsubClient.cpp \
src/singletons/WindowManager.cpp \ src/providers/twitch/PubsubHelpers.cpp \
src/util/DebugCount.cpp \ src/providers/twitch/TwitchAccount.cpp \
src/util/RapidjsonHelpers.cpp \ src/providers/twitch/TwitchAccountManager.cpp \
src/util/StreamLink.cpp \ src/providers/twitch/TwitchApi.cpp \
src/widgets/AccountSwitchPopupWidget.cpp \ src/providers/twitch/TwitchBadges.cpp \
src/widgets/AccountSwitchWidget.cpp \ src/providers/twitch/TwitchChannel.cpp \
src/widgets/AttachedWindow.cpp \ src/providers/twitch/TwitchEmotes.cpp \
src/widgets/dialogs/EmotePopup.cpp \ src/providers/twitch/TwitchHelpers.cpp \
src/widgets/dialogs/LastRunCrashDialog.cpp \ src/providers/twitch/TwitchMessageBuilder.cpp \
src/widgets/dialogs/LoginDialog.cpp \ src/providers/twitch/TwitchParseCheerEmotes.cpp \
src/widgets/dialogs/LogsPopup.cpp \ src/providers/twitch/TwitchServer.cpp \
src/widgets/dialogs/NotificationPopup.cpp \ src/providers/twitch/TwitchUser.cpp \
src/widgets/dialogs/QualityPopup.cpp \ src/RunGui.cpp \
src/widgets/dialogs/SelectChannelDialog.cpp \ src/singletons/Badges.cpp \
src/widgets/dialogs/SettingsDialog.cpp \ src/singletons/Emotes.cpp \
src/widgets/dialogs/TextInputDialog.cpp \ src/singletons/helper/GifTimer.cpp \
src/widgets/dialogs/UserInfoPopup.cpp \ src/singletons/helper/LoggingChannel.cpp \
src/widgets/dialogs/WelcomeDialog.cpp \ src/singletons/Logging.cpp \
src/widgets/helper/ChannelView.cpp \ src/singletons/NativeMessaging.cpp \
src/widgets/helper/ComboBoxItemDelegate.cpp \ src/singletons/Paths.cpp \
src/widgets/helper/DebugPopup.cpp \ src/singletons/Resources.cpp \
src/widgets/helper/EditableModelView.cpp \ src/singletons/Settings.cpp \
src/widgets/helper/NotebookButton.cpp \ src/singletons/Theme.cpp \
src/widgets/helper/NotebookTab.cpp \ src/singletons/Toasts.cpp \
src/widgets/helper/ResizingTextEdit.cpp \ src/singletons/Updates.cpp \
src/widgets/helper/ScrollbarHighlight.cpp \ src/singletons/WindowManager.cpp \
src/widgets/helper/SearchPopup.cpp \ src/singletons/TooltipPreviewImage.cpp \
src/widgets/helper/SettingsDialogTab.cpp \ src/util/DebugCount.cpp \
src/widgets/Notebook.cpp \ src/util/FormatTime.cpp \
src/widgets/Scrollbar.cpp \ src/util/IncognitoBrowser.cpp \
src/widgets/settingspages/AboutPage.cpp \ src/util/InitUpdateButton.cpp \
src/widgets/settingspages/AccountsPage.cpp \ src/util/JsonQuery.cpp \
src/widgets/settingspages/BrowserExtensionPage.cpp \ src/util/RapidjsonHelpers.cpp \
src/widgets/settingspages/CommandPage.cpp \ src/util/StreamLink.cpp \
src/widgets/settingspages/EmotesPage.cpp \ src/widgets/AccountSwitchPopupWidget.cpp \
src/widgets/settingspages/ExternalToolsPage.cpp \ src/widgets/AccountSwitchWidget.cpp \
src/widgets/settingspages/HighlightingPage.cpp \ src/widgets/AttachedWindow.cpp \
src/widgets/settingspages/KeyboardSettingsPage.cpp \ src/widgets/dialogs/EmotePopup.cpp \
src/widgets/settingspages/LogsPage.cpp \ src/widgets/dialogs/LastRunCrashDialog.cpp \
src/widgets/settingspages/ModerationPage.cpp \ src/widgets/dialogs/LoginDialog.cpp \
src/widgets/settingspages/NotificationPage.cpp \ src/widgets/dialogs/LogsPopup.cpp \
src/widgets/settingspages/SettingsPage.cpp \ src/widgets/dialogs/NotificationPopup.cpp \
src/widgets/settingspages/SpecialChannelsPage.cpp \ src/widgets/dialogs/QualityPopup.cpp \
src/widgets/splits/Split.cpp \ src/widgets/dialogs/SelectChannelDialog.cpp \
src/widgets/splits/SplitContainer.cpp \ src/widgets/dialogs/SettingsDialog.cpp \
src/widgets/splits/SplitHeader.cpp \ src/widgets/dialogs/TextInputDialog.cpp \
src/widgets/splits/SplitInput.cpp \ src/widgets/dialogs/UpdateDialog.cpp \
src/widgets/splits/SplitOverlay.cpp \ src/widgets/dialogs/UserInfoPopup.cpp \
src/widgets/StreamView.cpp \ src/widgets/dialogs/WelcomeDialog.cpp \
src/widgets/Window.cpp \ src/widgets/helper/ChannelView.cpp \
src/common/LinkParser.cpp \ src/widgets/helper/ComboBoxItemDelegate.cpp \
src/controllers/moderationactions/ModerationActions.cpp \ src/widgets/helper/DebugPopup.cpp \
src/singletons/NativeMessaging.cpp \ src/widgets/helper/EditableModelView.cpp \
src/singletons/Emotes.cpp \ src/widgets/helper/NotebookButton.cpp \
src/singletons/Logging.cpp \ src/widgets/helper/NotebookTab.cpp \
src/singletons/Paths.cpp \ src/widgets/helper/ResizingTextEdit.cpp \
src/singletons/Resources.cpp \ src/widgets/helper/ScrollbarHighlight.cpp \
src/singletons/Settings.cpp \ src/widgets/helper/SearchPopup.cpp \
src/singletons/Updates.cpp \ src/widgets/helper/SettingsDialogTab.cpp \
src/singletons/Theme.cpp \ src/widgets/Notebook.cpp \
src/controllers/moderationactions/ModerationActionModel.cpp \ src/widgets/Scrollbar.cpp \
src/widgets/settingspages/LookPage.cpp \ src/widgets/settingspages/AboutPage.cpp \
src/widgets/settingspages/FeelPage.cpp \ src/widgets/settingspages/AccountsPage.cpp \
src/util/InitUpdateButton.cpp \ src/widgets/settingspages/AdvancedPage.cpp \
src/widgets/dialogs/UpdateDialog.cpp \ src/widgets/settingspages/BrowserExtensionPage.cpp \
src/widgets/settingspages/IgnoresPage.cpp \ src/widgets/settingspages/CommandPage.cpp \
src/providers/twitch/PubsubClient.cpp \ src/widgets/settingspages/EmotesPage.cpp \
src/providers/twitch/TwitchApi.cpp \ src/widgets/settingspages/ExternalToolsPage.cpp \
src/messages/Emote.cpp \ src/widgets/settingspages/FeelPage.cpp \
src/messages/ImageSet.cpp \ src/widgets/settingspages/GeneralPage.cpp \
src/providers/bttv/BttvEmotes.cpp \ src/widgets/settingspages/HighlightingPage.cpp \
src/providers/LinkResolver.cpp \ src/widgets/settingspages/IgnoresPage.cpp \
src/providers/ffz/FfzEmotes.cpp \ src/widgets/settingspages/KeyboardSettingsPage.cpp \
src/autogenerated/ResourcesAutogen.cpp \ src/widgets/settingspages/LogsPage.cpp \
src/singletons/Badges.cpp \ src/widgets/settingspages/LookPage.cpp \
src/providers/twitch/TwitchBadges.cpp \ src/widgets/settingspages/ModerationPage.cpp \
src/providers/chatterino/ChatterinoBadges.cpp \ src/widgets/settingspages/NotificationPage.cpp \
src/providers/twitch/TwitchParseCheerEmotes.cpp \ src/widgets/settingspages/SettingsPage.cpp \
src/providers/bttv/LoadBttvChannelEmote.cpp \ src/widgets/settingspages/SpecialChannelsPage.cpp \
src/util/JsonQuery.cpp \ src/widgets/splits/ClosedSplits.cpp \
src/RunGui.cpp \ src/widgets/splits/Split.cpp \
src/BrowserExtension.cpp \ src/widgets/splits/SplitContainer.cpp \
src/util/FormatTime.cpp \ src/widgets/splits/SplitHeader.cpp \
src/controllers/notifications/NotificationModel.cpp \ src/widgets/splits/SplitInput.cpp \
src/singletons/Toasts.cpp \ src/widgets/splits/SplitOverlay.cpp \
src/common/DownloadManager.cpp \ src/widgets/StreamView.cpp \
src/messages/MessageContainer.cpp \ src/widgets/Window.cpp \
src/common/UsernameSet.cpp \ src/controllers/pings/PingController.cpp \
src/widgets/settingspages/AdvancedPage.cpp \ src/controllers/pings/PingModel.cpp \
src/util/IncognitoBrowser.cpp \
src/widgets/splits/ClosedSplits.cpp \ HEADERS += \
src/providers/ffz/FfzModBadge.cpp \ src/Application.hpp \
src/widgets/settingspages/GeneralPage.cpp \ src/autogenerated/ResourcesAutogen.hpp \
src/providers/twitch/ChatroomChannel.cpp src/BrowserExtension.hpp \
src/common/Aliases.hpp \
HEADERS += \ src/common/Atomic.hpp \
src/Application.hpp \ src/common/Channel.hpp \
src/common/Channel.hpp \ src/common/Common.hpp \
src/common/Common.hpp \ src/common/CompletionModel.hpp \
src/common/CompletionModel.hpp \ src/common/ConcurrentMap.hpp \
src/common/Atomic.hpp \ src/common/DownloadManager.hpp \
src/common/NetworkCommon.hpp \ src/common/LinkParser.hpp \
src/common/NetworkData.hpp \ src/common/NetworkCommon.hpp \
src/common/NetworkManager.hpp \ src/common/NetworkData.hpp \
src/common/NetworkRequest.hpp \ src/common/NetworkManager.hpp \
src/common/NetworkRequester.hpp \ src/common/NetworkRequest.hpp \
src/common/NetworkResult.hpp \ src/common/NetworkRequester.hpp \
src/common/NetworkTimer.hpp \ src/common/NetworkResult.hpp \
src/common/NetworkWorker.hpp \ src/common/NetworkTimer.hpp \
src/common/NullablePtr.hpp \ src/common/NetworkWorker.hpp \
src/common/ProviderId.hpp \ src/common/NullablePtr.hpp \
src/common/SignalVectorModel.hpp \ src/common/ProviderId.hpp \
src/common/Version.hpp \ src/common/SignalVector.hpp \
src/controllers/accounts/Account.hpp \ src/common/SignalVectorModel.hpp \
src/controllers/accounts/AccountController.hpp \ src/common/UniqueAccess.hpp \
src/controllers/accounts/AccountModel.hpp \ src/common/UsernameSet.hpp \
src/controllers/commands/Command.hpp \ src/common/Version.hpp \
src/controllers/commands/CommandController.hpp \ src/controllers/accounts/Account.hpp \
src/controllers/commands/CommandModel.hpp \ src/controllers/accounts/AccountController.hpp \
src/controllers/highlights/HighlightController.hpp \ src/controllers/accounts/AccountModel.hpp \
src/controllers/highlights/HighlightModel.hpp \ src/controllers/commands/Command.hpp \
src/controllers/highlights/HighlightBlacklistModel.hpp \ src/controllers/commands/CommandController.hpp \
src/controllers/highlights/HighlightPhrase.hpp \ src/controllers/commands/CommandModel.hpp \
src/controllers/highlights/HighlightBlacklistUser.hpp \ src/controllers/highlights/HighlightBlacklistModel.hpp \
src/controllers/highlights/UserHighlightModel.hpp \ src/controllers/highlights/HighlightBlacklistUser.hpp \
src/controllers/ignores/IgnoreController.hpp \ src/controllers/highlights/HighlightController.hpp \
src/controllers/ignores/IgnoreModel.hpp \ src/controllers/highlights/HighlightModel.hpp \
src/controllers/ignores/IgnorePhrase.hpp \ src/controllers/highlights/HighlightPhrase.hpp \
src/controllers/notifications/NotificationController.hpp \ src/controllers/highlights/UserHighlightModel.hpp \
src/controllers/taggedusers/TaggedUser.hpp \ src/controllers/ignores/IgnoreController.hpp \
src/controllers/taggedusers/TaggedUsersController.hpp \ src/controllers/ignores/IgnoreModel.hpp \
src/controllers/taggedusers/TaggedUsersModel.hpp \ src/controllers/ignores/IgnorePhrase.hpp \
src/messages/Image.hpp \ src/controllers/moderationactions/ModerationAction.hpp \
src/messages/layouts/MessageLayout.hpp \ src/controllers/moderationactions/ModerationActionModel.hpp \
src/messages/layouts/MessageLayoutContainer.hpp \ src/controllers/moderationactions/ModerationActions.hpp \
src/messages/layouts/MessageLayoutElement.hpp \ src/controllers/notifications/NotificationController.hpp \
src/messages/LimitedQueue.hpp \ src/controllers/notifications/NotificationModel.hpp \
src/messages/LimitedQueueSnapshot.hpp \ src/controllers/taggedusers/TaggedUser.hpp \
src/messages/Link.hpp \ src/controllers/taggedusers/TaggedUsersController.hpp \
src/messages/Message.hpp \ src/controllers/taggedusers/TaggedUsersModel.hpp \
src/messages/MessageBuilder.hpp \ src/messages/Emote.hpp \
src/messages/MessageColor.hpp \ src/messages/Image.hpp \
src/messages/MessageElement.hpp \ src/messages/ImageSet.hpp \
src/messages/Selection.hpp \ src/messages/layouts/MessageLayout.hpp \
src/PrecompiledHeader.hpp \ src/messages/layouts/MessageLayoutContainer.hpp \
src/providers/emoji/Emojis.hpp \ src/messages/layouts/MessageLayoutElement.hpp \
src/providers/irc/AbstractIrcServer.hpp \ src/messages/LimitedQueue.hpp \
src/providers/irc/IrcAccount.hpp \ src/messages/LimitedQueueSnapshot.hpp \
src/providers/irc/IrcChannel2.hpp \ src/messages/Link.hpp \
src/providers/irc/IrcConnection2.hpp \ src/messages/Message.hpp \
src/providers/irc/IrcServer.hpp \ src/messages/MessageBuilder.hpp \
src/providers/twitch/EmoteValue.hpp \ src/messages/MessageColor.hpp \
src/providers/twitch/IrcMessageHandler.hpp \ src/messages/MessageContainer.hpp \
src/providers/twitch/PartialTwitchUser.hpp \ src/messages/MessageElement.hpp \
src/providers/twitch/PubsubActions.hpp \ src/messages/MessageParseArgs.hpp \
src/providers/twitch/PubsubHelpers.hpp \ src/messages/Selection.hpp \
src/providers/twitch/TwitchAccount.hpp \ src/PrecompiledHeader.hpp \
src/providers/twitch/TwitchAccountManager.hpp \ src/providers/bttv/BttvEmotes.hpp \
src/providers/twitch/TwitchChannel.hpp \ src/providers/bttv/LoadBttvChannelEmote.hpp \
src/providers/twitch/TwitchEmotes.hpp \ src/providers/chatterino/ChatterinoBadges.hpp \
src/providers/twitch/TwitchHelpers.hpp \ src/providers/emoji/Emojis.hpp \
src/providers/twitch/TwitchMessageBuilder.hpp \ src/providers/ffz/FfzEmotes.hpp \
src/providers/twitch/TwitchServer.hpp \ src/providers/ffz/FfzModBadge.hpp \
src/providers/twitch/TwitchUser.hpp \ src/providers/irc/AbstractIrcServer.hpp \
src/singletons/helper/GifTimer.hpp \ src/providers/irc/IrcAccount.hpp \
src/singletons/helper/LoggingChannel.hpp \ src/providers/irc/IrcChannel2.hpp \
src/controllers/moderationactions/ModerationAction.hpp \ src/providers/irc/IrcConnection2.hpp \
src/singletons/WindowManager.hpp \ src/providers/irc/IrcServer.hpp \
src/util/ConcurrentMap.hpp \ src/providers/LinkResolver.hpp \
src/util/DebugCount.hpp \ src/providers/twitch/ChatroomChannel.hpp \
src/util/IrcHelpers.hpp \ src/providers/twitch/EmoteValue.hpp \
src/util/LayoutCreator.hpp \ src/providers/twitch/IrcMessageHandler.hpp \
src/util/QStringHash.hpp \ src/providers/twitch/PartialTwitchUser.hpp \
src/util/RapidjsonHelpers.hpp \ src/providers/twitch/PubsubActions.hpp \
src/util/RemoveScrollAreaBackground.hpp \ src/providers/twitch/PubsubClient.hpp \
src/util/SharedPtrElementLess.hpp \ src/providers/twitch/PubsubHelpers.hpp \
src/util/StandardItemHelper.hpp \ src/providers/twitch/TwitchAccount.hpp \
src/util/StreamLink.hpp \ src/providers/twitch/TwitchAccountManager.hpp \
src/widgets/AccountSwitchPopupWidget.hpp \ src/providers/twitch/TwitchApi.hpp \
src/widgets/AccountSwitchWidget.hpp \ src/providers/twitch/TwitchBadges.hpp \
src/widgets/AttachedWindow.hpp \ src/providers/twitch/TwitchChannel.hpp \
src/widgets/dialogs/EmotePopup.hpp \ src/providers/twitch/TwitchCommon.hpp \
src/widgets/dialogs/LastRunCrashDialog.hpp \ src/providers/twitch/TwitchEmotes.hpp \
src/widgets/dialogs/LoginDialog.hpp \ src/providers/twitch/TwitchHelpers.hpp \
src/widgets/dialogs/LogsPopup.hpp \ src/providers/twitch/TwitchMessageBuilder.hpp \
src/widgets/dialogs/NotificationPopup.hpp \ src/providers/twitch/TwitchParseCheerEmotes.hpp \
src/widgets/dialogs/QualityPopup.hpp \ src/providers/twitch/TwitchServer.hpp \
src/widgets/dialogs/SelectChannelDialog.hpp \ src/providers/twitch/TwitchUser.hpp \
src/widgets/dialogs/SettingsDialog.hpp \ src/RunGui.hpp \
src/widgets/dialogs/TextInputDialog.hpp \ src/singletons/TooltipPreviewImage.hpp \
src/widgets/dialogs/UserInfoPopup.hpp \ src/singletons/Badges.hpp \
src/widgets/dialogs/WelcomeDialog.hpp \ src/singletons/Emotes.hpp \
src/widgets/helper/ChannelView.hpp \ src/singletons/helper/GifTimer.hpp \
src/widgets/helper/ComboBoxItemDelegate.hpp \ src/singletons/helper/LoggingChannel.hpp \
src/widgets/helper/DebugPopup.hpp \ src/singletons/Logging.hpp \
src/widgets/helper/EditableModelView.hpp \ src/singletons/NativeMessaging.hpp \
src/widgets/helper/Line.hpp \ src/singletons/Paths.hpp \
src/widgets/helper/NotebookButton.hpp \ src/singletons/Resources.hpp \
src/widgets/helper/NotebookTab.hpp \ src/singletons/Settings.hpp \
src/widgets/helper/ResizingTextEdit.hpp \ src/singletons/Theme.hpp \
src/widgets/helper/ScrollbarHighlight.hpp \ src/singletons/Toasts.hpp \
src/widgets/helper/SearchPopup.hpp \ src/singletons/Updates.hpp \
src/widgets/helper/SettingsDialogTab.hpp \ src/singletons/WindowManager.hpp \
src/widgets/Notebook.hpp \ src/util/ConcurrentMap.hpp \
src/widgets/Scrollbar.hpp \ src/util/DebugCount.hpp \
src/widgets/settingspages/AboutPage.hpp \ src/util/FormatTime.hpp \
src/widgets/settingspages/AccountsPage.hpp \ src/util/IncognitoBrowser.hpp \
src/widgets/settingspages/BrowserExtensionPage.hpp \ src/util/InitUpdateButton.hpp \
src/widgets/settingspages/CommandPage.hpp \ src/util/IrcHelpers.hpp \
src/widgets/settingspages/EmotesPage.hpp \ src/util/IsBigEndian.hpp \
src/widgets/settingspages/ExternalToolsPage.hpp \ src/util/JsonQuery.hpp \
src/widgets/settingspages/HighlightingPage.hpp \ src/util/LayoutCreator.hpp \
src/widgets/settingspages/KeyboardSettingsPage.hpp \ src/util/QStringHash.hpp \
src/widgets/settingspages/LogsPage.hpp \ src/util/rangealgorithm.hpp \
src/widgets/settingspages/ModerationPage.hpp \ src/util/RapidjsonHelpers.hpp \
src/widgets/settingspages/NotificationPage.hpp \ src/util/RemoveScrollAreaBackground.hpp \
src/widgets/settingspages/SettingsPage.hpp \ src/util/SharedPtrElementLess.hpp \
src/widgets/settingspages/SpecialChannelsPage.hpp \ src/util/StandardItemHelper.hpp \
src/widgets/splits/Split.hpp \ src/util/StreamLink.hpp \
src/widgets/splits/SplitContainer.hpp \ src/widgets/AccountSwitchPopupWidget.hpp \
src/widgets/splits/SplitHeader.hpp \ src/widgets/AccountSwitchWidget.hpp \
src/widgets/splits/SplitInput.hpp \ src/widgets/AttachedWindow.hpp \
src/widgets/splits/SplitOverlay.hpp \ src/widgets/dialogs/EmotePopup.hpp \
src/widgets/StreamView.hpp \ src/widgets/dialogs/LastRunCrashDialog.hpp \
src/widgets/Window.hpp \ src/widgets/dialogs/LoginDialog.hpp \
src/providers/twitch/TwitchCommon.hpp \ src/widgets/dialogs/LogsPopup.hpp \
src/util/IsBigEndian.hpp \ src/widgets/dialogs/NotificationPopup.hpp \
src/common/LinkParser.hpp \ src/widgets/dialogs/QualityPopup.hpp \
src/controllers/moderationactions/ModerationActions.hpp \ src/widgets/dialogs/SelectChannelDialog.hpp \
src/singletons/Emotes.hpp \ src/widgets/dialogs/SettingsDialog.hpp \
src/singletons/Logging.hpp \ src/widgets/dialogs/TextInputDialog.hpp \
src/singletons/Paths.hpp \ src/widgets/dialogs/UpdateDialog.hpp \
src/singletons/Resources.hpp \ src/widgets/dialogs/UserInfoPopup.hpp \
src/singletons/Settings.hpp \ src/widgets/dialogs/WelcomeDialog.hpp \
src/singletons/Updates.hpp \ src/widgets/helper/ChannelView.hpp \
src/singletons/NativeMessaging.hpp \ src/widgets/helper/ComboBoxItemDelegate.hpp \
src/singletons/Theme.hpp \ src/widgets/helper/CommonTexts.hpp \
src/common/SignalVector.hpp \ src/widgets/helper/DebugPopup.hpp \
src/widgets/dialogs/LogsPopup.hpp \ src/widgets/helper/EditableModelView.hpp \
src/controllers/moderationactions/ModerationActionModel.hpp \ src/widgets/helper/Line.hpp \
src/widgets/settingspages/LookPage.hpp \ src/widgets/helper/NotebookButton.hpp \
src/widgets/settingspages/FeelPage.hpp \ src/widgets/helper/NotebookTab.hpp \
src/util/InitUpdateButton.hpp \ src/widgets/helper/ResizingTextEdit.hpp \
src/widgets/dialogs/UpdateDialog.hpp \ src/widgets/helper/ScrollbarHighlight.hpp \
src/widgets/settingspages/IgnoresPage.hpp \ src/widgets/helper/SearchPopup.hpp \
src/providers/twitch/PubsubClient.hpp \ src/widgets/helper/SettingsDialogTab.hpp \
src/providers/twitch/TwitchApi.hpp \ src/widgets/Notebook.hpp \
src/messages/Emote.hpp \ src/widgets/Scrollbar.hpp \
src/messages/ImageSet.hpp \ src/widgets/settingspages/AboutPage.hpp \
src/providers/bttv/BttvEmotes.hpp \ src/widgets/settingspages/AccountsPage.hpp \
src/providers/LinkResolver.hpp \ src/widgets/settingspages/AdvancedPage.hpp \
src/providers/ffz/FfzEmotes.hpp \ src/widgets/settingspages/BrowserExtensionPage.hpp \
src/autogenerated/ResourcesAutogen.hpp \ src/widgets/settingspages/CommandPage.hpp \
src/singletons/Badges.hpp \ src/widgets/settingspages/EmotesPage.hpp \
src/providers/twitch/TwitchBadges.hpp \ src/widgets/settingspages/ExternalToolsPage.hpp \
src/providers/chatterino/ChatterinoBadges.hpp \ src/widgets/settingspages/FeelPage.hpp \
src/common/Aliases.hpp \ src/widgets/settingspages/GeneralPage.hpp \
src/providers/twitch/TwitchParseCheerEmotes.hpp \ src/widgets/settingspages/HighlightingPage.hpp \
src/providers/bttv/LoadBttvChannelEmote.hpp \ src/widgets/settingspages/IgnoresPage.hpp \
src/util/JsonQuery.hpp \ src/widgets/settingspages/KeyboardSettingsPage.hpp \
src/RunGui.hpp \ src/widgets/settingspages/LogsPage.hpp \
src/BrowserExtension.hpp \ src/widgets/settingspages/LookPage.hpp \
src/util/FormatTime.hpp \ src/widgets/settingspages/ModerationPage.hpp \
src/controllers/notifications/NotificationModel.hpp \ src/widgets/settingspages/NotificationPage.hpp \
src/singletons/Toasts.hpp \ src/widgets/settingspages/SettingsPage.hpp \
src/common/DownloadManager.hpp \ src/widgets/settingspages/SpecialChannelsPage.hpp \
src/messages/MessageContainer.hpp \ src/widgets/splits/ClosedSplits.hpp \
src/common/UsernameSet.hpp \ src/widgets/splits/Split.hpp \
src/widgets/settingspages/AdvancedPage.hpp \ src/widgets/splits/SplitContainer.hpp \
src/util/IncognitoBrowser.hpp \ src/widgets/splits/SplitHeader.hpp \
src/widgets/splits/ClosedSplits.hpp \ src/widgets/splits/SplitInput.hpp \
src/providers/ffz/FfzModBadge.hpp \ src/widgets/splits/SplitOverlay.hpp \
src/widgets/settingspages/GeneralPage.hpp \ src/widgets/StreamView.hpp \
src/messages/HistoricMessageAppearance.hpp \ src/widgets/Window.hpp \
src/providers/twitch/ChatroomChannel.hpp src/controllers/pings/PingController.hpp \
src/controllers/pings/PingModel.hpp \
RESOURCES += \
resources/resources.qrc \ RESOURCES += \
resources/resources_autogenerated.qrc resources/resources.qrc \
resources/resources_autogenerated.qrc
DISTFILES +=
DISTFILES +=
FORMS +=
FORMS +=
# do not use windows min/max macros
#win32 { # do not use windows min/max macros
# DEFINES += NOMINMAX #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
}

20
docs/ENV.md Normal file
View file

@ -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

@ -1 +1 @@
Subproject commit e6a31d5228ed8969596d6fdbd030f71a7a17f30d Subproject commit d054925734cf26576346a1da856f7ab0d4b6c0a5

1
resources/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
linuxinstall

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32mm"
height="32mm"
viewBox="0 0 32 32"
version="1.1"
id="svg8"
inkscape:export-filename="/home/pajlada/git/chatterino2/resources/buttons/trashcan2.png"
inkscape:export-xdpi="50.799999"
inkscape:export-ydpi="50.799999"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="trashcan.svg">
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient4536"
osb:paint="gradient">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4532" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop4534" />
</linearGradient>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="58.051498"
inkscape:cy="84.215087"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1529"
inkscape:window-height="1419"
inkscape:window-x="2160"
inkscape:window-y="2400"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid4818"
originx="-74.790005"
originy="-199.8473"
units="mm"
spacingx="1"
spacingy="1" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<cc:license
rdf:resource="" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-74.789996,-65.152683)">
<path
style="fill:none;stroke:#898395;stroke-width:3.5999999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 82.789996,69.152684 v 25 h 16 v -25"
id="path4820"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#898395;stroke-width:3.6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 102.79,69.152684 H 78.789994"
id="path4826"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc">
<title
id="title4970">Trashcan top</title>
</path>
<path
style="fill:none;stroke:#898395;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 87.789996,74.999984 v 14"
id="path4830"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;stroke:#898395;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 93.789996,88.999984 v -14"
id="path4832"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<circle
style="fill:#898395;fill-opacity:1;stroke:none;stroke-width:6.75056219;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
id="path4974"
cx="90.789993"
cy="67.069061"
r="1.75" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-74.789996,-65.152683)" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -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;

View file

@ -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. # 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 # this will ignore a/b/c/d.txt and a/b/xd.txt
ignored_directories = ['__pycache__'] ignored_directories = ['__pycache__', 'linuxinstall']
def isNotIgnored(file): def isNotIgnored(file):
# check if file exists in an ignored direcory # check if file exists in an ignored direcory

View file

@ -1,5 +1,5 @@
<RCC> <RCC>
<qresource prefix="/"> <file>chatterino2.icns</file> <qresource prefix="/"> <file>chatterino.icns</file>
<file>contributors.txt</file> <file>contributors.txt</file>
<file>emoji.json</file> <file>emoji.json</file>
<file>emojidata.txt</file> <file>emojidata.txt</file>
@ -24,6 +24,7 @@
<file>buttons/modModeEnabled.png</file> <file>buttons/modModeEnabled.png</file>
<file>buttons/modModeEnabled2.png</file> <file>buttons/modModeEnabled2.png</file>
<file>buttons/timeout.png</file> <file>buttons/timeout.png</file>
<file>buttons/trashCan.png</file>
<file>buttons/unban.png</file> <file>buttons/unban.png</file>
<file>buttons/unmod.png</file> <file>buttons/unmod.png</file>
<file>buttons/update.png</file> <file>buttons/update.png</file>

View file

@ -6,6 +6,7 @@
#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnoreController.hpp"
#include "controllers/moderationactions/ModerationActions.hpp" #include "controllers/moderationactions/ModerationActions.hpp"
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "controllers/pings/PingController.hpp"
#include "controllers/taggedusers/TaggedUsersController.hpp" #include "controllers/taggedusers/TaggedUsersController.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
@ -53,6 +54,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, commands(&this->emplace<CommandController>()) , commands(&this->emplace<CommandController>())
, highlights(&this->emplace<HighlightController>()) , highlights(&this->emplace<HighlightController>())
, notifications(&this->emplace<NotificationController>()) , notifications(&this->emplace<NotificationController>())
, pings(&this->emplace<PingController>())
, ignores(&this->emplace<IgnoreController>()) , ignores(&this->emplace<IgnoreController>())
, taggedUsers(&this->emplace<TaggedUsersController>()) , taggedUsers(&this->emplace<TaggedUsersController>())
, moderationActions(&this->emplace<ModerationActions>()) , moderationActions(&this->emplace<ModerationActions>())
@ -264,6 +266,7 @@ void Application::initPubsub()
auto msg = MessageBuilder(action).release(); auto msg = MessageBuilder(action).release();
postToThread([chan, msg] { chan->addMessage(msg); }); postToThread([chan, msg] { chan->addMessage(msg); });
chan->deleteMessage(msg->id);
}); });
this->twitch.pubsub->start(); this->twitch.pubsub->start();

View file

@ -18,6 +18,7 @@ class TaggedUsersController;
class AccountController; class AccountController;
class ModerationActions; class ModerationActions;
class NotificationController; class NotificationController;
class PingController;
class Theme; class Theme;
class WindowManager; class WindowManager;
@ -62,6 +63,7 @@ public:
CommandController *const commands{}; CommandController *const commands{};
HighlightController *const highlights{}; HighlightController *const highlights{};
NotificationController *const notifications{}; NotificationController *const notifications{};
PingController *const pings{};
IgnoreController *const ignores{}; IgnoreController *const ignores{};
TaggedUsersController *const taggedUsers{}; TaggedUsersController *const taggedUsers{};
ModerationActions *const moderationActions{}; ModerationActions *const moderationActions{};

View file

@ -18,6 +18,7 @@ Resources2::Resources2()
this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png"); this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png");
this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png"); this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png");
this->buttons.timeout = QPixmap(":/buttons/timeout.png"); this->buttons.timeout = QPixmap(":/buttons/timeout.png");
this->buttons.trashCan = QPixmap(":/buttons/trashCan.png");
this->buttons.unban = QPixmap(":/buttons/unban.png"); this->buttons.unban = QPixmap(":/buttons/unban.png");
this->buttons.unmod = QPixmap(":/buttons/unmod.png"); this->buttons.unmod = QPixmap(":/buttons/unmod.png");
this->buttons.update = QPixmap(":/buttons/update.png"); this->buttons.update = QPixmap(":/buttons/update.png");

View file

@ -24,6 +24,7 @@ public:
QPixmap modModeEnabled; QPixmap modModeEnabled;
QPixmap modModeEnabled2; QPixmap modModeEnabled2;
QPixmap timeout; QPixmap timeout;
QPixmap trashCan;
QPixmap unban; QPixmap unban;
QPixmap unmod; QPixmap unmod;
QPixmap update; QPixmap update;

View file

@ -78,7 +78,7 @@ void Channel::addMessage(MessagePtr message,
// FOURTF: change this when adding more providers // FOURTF: change this when adding more providers
if (this->isTwitchChannel() && if (this->isTwitchChannel() &&
(!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))) (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog)))
{ {
app->logging->addMessage(this->name_, message); app->logging->addMessage(this->name_, message);
} }
@ -154,8 +154,9 @@ void Channel::addOrReplaceTimeout(MessagePtr message)
for (int i = 0; i < snapshotLength; i++) for (int i = 0; i < snapshotLength; i++)
{ {
auto &s = snapshot[i]; auto &s = snapshot[i];
if (s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout}) && if (s->loginName == message->timeoutUser &&
s->loginName == message->timeoutUser) s->flags.hasNone({MessageFlag::Timeout, MessageFlag::Untimeout,
MessageFlag::Whisper}))
{ {
// FOURTF: disabled for now // FOURTF: disabled for now
// PAJLADA: Shitty solution described in Message.hpp // PAJLADA: Shitty solution described in Message.hpp
@ -179,7 +180,8 @@ void Channel::disableAllMessages()
for (int i = 0; i < snapshotLength; i++) for (int i = 0; i < snapshotLength; i++)
{ {
auto &message = snapshot[i]; auto &message = snapshot[i];
if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout})) if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout,
MessageFlag::Whisper}))
{ {
continue; continue;
} }
@ -210,6 +212,25 @@ void Channel::replaceMessage(MessagePtr message, MessagePtr replacement)
} }
} }
void Channel::deleteMessage(QString messageID)
{
LimitedQueueSnapshot<MessagePtr> 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) void Channel::addRecentChatter(const MessagePtr &message)
{ {
} }
@ -239,6 +260,11 @@ bool Channel::hasModRights() const
return this->isMod() || this->isBroadcaster(); return this->isMod() || this->isBroadcaster();
} }
bool Channel::hasHighRateLimit() const
{
return this->isMod() || this->isBroadcaster();
}
bool Channel::isLive() const bool Channel::isLive() const
{ {
return false; return false;

View file

@ -15,7 +15,7 @@ namespace chatterino {
struct Message; struct Message;
using MessagePtr = std::shared_ptr<const Message>; using MessagePtr = std::shared_ptr<const Message>;
enum class MessageFlag : uint16_t; enum class MessageFlag : uint32_t;
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;
class Channel : public std::enable_shared_from_this<Channel> class Channel : public std::enable_shared_from_this<Channel>
@ -62,6 +62,7 @@ public:
void addOrReplaceTimeout(MessagePtr message); void addOrReplaceTimeout(MessagePtr message);
void disableAllMessages(); void disableAllMessages();
void replaceMessage(MessagePtr message, MessagePtr replacement); void replaceMessage(MessagePtr message, MessagePtr replacement);
void deleteMessage(QString messageID);
QStringList modList; QStringList modList;
@ -70,6 +71,7 @@ public:
virtual bool isMod() const; virtual bool isMod() const;
virtual bool isBroadcaster() const; virtual bool isBroadcaster() const;
virtual bool hasModRights() const; virtual bool hasModRights() const;
virtual bool hasHighRateLimit() const;
virtual bool isLive() const; virtual bool isLive() const;
virtual bool shouldIgnoreHighlights() const; virtual bool shouldIgnoreHighlights() const;

View file

@ -20,9 +20,7 @@ DownloadManager::~DownloadManager()
void DownloadManager::setFile(QString fileURL, const QString &channelName) void DownloadManager::setFile(QString fileURL, const QString &channelName)
{ {
QString filePath = fileURL;
QString saveFilePath; QString saveFilePath;
QStringList filePathList = filePath.split('/');
saveFilePath = saveFilePath =
getPaths()->twitchProfileAvatars + "/twitch/" + channelName + ".png"; getPaths()->twitchProfileAvatars + "/twitch/" + channelName + ".png";
QNetworkRequest request; QNetworkRequest request;

40
src/common/Env.cpp Normal file
View file

@ -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

19
src/common/Env.hpp Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#include <QString>
namespace chatterino {
class Env
{
Env();
public:
static const Env &get();
const QString recentMessagesApiUrl;
const QString linkResolverUrl;
const QString twitchEmoteSetResolverUrl;
};
} // namespace chatterino

View file

@ -188,7 +188,7 @@ public:
assert(row >= 0 && row < this->rows_.size() && column >= 0 && assert(row >= 0 && row < this->rows_.size() && column >= 0 &&
column < this->columnCount_); 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) QStandardItem *getItem(int row, int column)

View file

@ -59,7 +59,7 @@ void UsernameSet::insertPrefix(const QString &value)
{ {
auto &string = this->firstKeyForPrefix[Prefix(value)]; auto &string = this->firstKeyForPrefix[Prefix(value)];
if (string.isNull() || value < string) if (string.isNull() || value.compare(string, Qt::CaseInsensitive) < 0)
string = value; string = value;
} }
@ -99,6 +99,11 @@ bool Prefix::operator==(const Prefix &other) const
std::tie(other.first, other.second); std::tie(other.first, other.second);
} }
bool Prefix::operator!=(const Prefix &other) const
{
return !(*this == other);
}
bool Prefix::isStartOf(const QString &string) const bool Prefix::isStartOf(const QString &string) const
{ {
if (string.size() == 0) if (string.size() == 0)

View file

@ -6,11 +6,13 @@
#include <unordered_map> #include <unordered_map>
namespace chatterino { namespace chatterino {
class Prefix class Prefix
{ {
public: public:
Prefix(const QString &string); Prefix(const QString &string);
bool operator==(const Prefix &other) const; bool operator==(const Prefix &other) const;
bool operator!=(const Prefix &other) const;
bool isStartOf(const QString &string) const; bool isStartOf(const QString &string) const;
private: private:
@ -19,9 +21,11 @@ private:
friend struct std::hash<Prefix>; friend struct std::hash<Prefix>;
}; };
} // namespace chatterino } // namespace chatterino
namespace std { namespace std {
template <> template <>
struct hash<chatterino::Prefix> { struct hash<chatterino::Prefix> {
size_t operator()(const chatterino::Prefix &prefix) const size_t operator()(const chatterino::Prefix &prefix) const
@ -30,9 +34,18 @@ struct hash<chatterino::Prefix> {
size_t(prefix.second.unicode()); size_t(prefix.second.unicode());
} }
}; };
} // namespace std } // namespace std
namespace chatterino { namespace chatterino {
struct CaseInsensitiveLess {
bool operator()(const QString &lhs, const QString &rhs) const
{
return lhs.compare(rhs, Qt::CaseInsensitive) < 0;
}
};
class UsernameSet class UsernameSet
{ {
public: public:
@ -66,7 +79,7 @@ public:
private: private:
void insertPrefix(const QString &string); void insertPrefix(const QString &string);
std::set<QString> items; std::set<QString, CaseInsensitiveLess> items;
std::unordered_map<Prefix, QString> firstKeyForPrefix; std::unordered_map<Prefix, QString> firstKeyForPrefix;
}; };

View file

@ -31,12 +31,12 @@ int AccountModel::beforeInsert(const std::shared_ptr<Account> &item,
{ {
if (this->categoryCount_[item->getCategory()]++ == 0) if (this->categoryCount_[item->getCategory()]++ == 0)
{ {
auto row = this->createRow(); auto newRow = this->createRow();
setStringItem(row[0], item->getCategory(), false, false); setStringItem(newRow[0], item->getCategory(), false, false);
row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole); 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; return proposedIndex + 1;
} }

View file

@ -15,6 +15,7 @@
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/CombinePath.hpp" #include "util/CombinePath.hpp"
#include "widgets/dialogs/LogsPopup.hpp" #include "widgets/dialogs/LogsPopup.hpp"
@ -28,9 +29,143 @@
"/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \ "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
"/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \ "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
"/clear", "/subscribers", "/subscribersoff", "/followers", \ "/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<TimestampElement>();
b.emplace<TextElement>(app->accounts->twitch.getCurrent()->getUserName(),
MessageElementFlag::Text, MessageColor::Text,
FontStyle::ChatMediumBold);
b.emplace<TextElement>("->", MessageElementFlag::Text,
getApp()->themes->messages.textColors.system);
b.emplace<TextElement>(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<EmotePtr>{};
for (int i = 2; i < words.length(); i++)
{
{ // twitch emote
auto it = accemotes.emotes.find({words[i]});
if (it != accemotes.emotes.end())
{
b.emplace<EmoteElement>(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<EmoteElement>(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<EmoteElement>(emote,
MessageElementFlag::EmojiAll);
}
void operator()(const QString &string,
MessageBuilder &b) const
{
auto linkString = b.matchLink(string);
if (linkString.isEmpty())
{
b.emplace<TextElement>(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<MessageFlags>(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 { namespace chatterino {
void CommandController::initialize(Settings &, Paths &paths) 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... // works in a valid twitch channel and /whispers, etc...
if (!dryRun && channel->isTwitchChannel()) if (!dryRun && channel->isTwitchChannel())
{ {
if (commandName == "/w") if (whisperCommands.contains(commandName, Qt::CaseInsensitive))
{ {
if (words.length() <= 2) if (words.length() > 2)
{ {
return ""; appendWhisperMessageWordsLocally(words);
} sendWhisperMessage(text);
auto app = getApp();
MessageBuilder b;
b.emplace<TimestampElement>();
b.emplace<TextElement>(
app->accounts->twitch.getCurrent()->getUserName(),
MessageElementFlag::Text, MessageColor::Text,
FontStyle::ChatMediumBold);
b.emplace<TextElement>("->", MessageElementFlag::Text);
b.emplace<TextElement>(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<EmotePtr>{};
for (int i = 2; i < words.length(); i++)
{
{ // twitch emote
auto it = accemotes.emotes.find({words[i]});
if (it != accemotes.emotes.end())
{
b.emplace<EmoteElement>(
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<EmoteElement>(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<EmoteElement>(
emote, MessageElementFlag::EmojiAll);
}
void operator()(const QString &string,
MessageBuilder &b) const
{
b.emplace<TextElement>(
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<MessageFlags>(messagexD->flags);
overrideFlags->set(MessageFlag::DoNotLog);
if (getSettings()->inlineWhispers)
{
app->twitch.server->forEachChannel(
[&messagexD, overrideFlags](ChannelPtr _channel) {
_channel->addMessage(messagexD, overrideFlags);
});
} }
return ""; return "";
@ -413,13 +461,36 @@ QString CommandController::execCommand(const QString &textNoEmoji,
logs->show(); logs->show();
return ""; 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); auto maxSpaces = std::min(this->maxSpaces_, words.length() - 1);
@ -427,10 +498,10 @@ QString CommandController::execCommand(const QString &textNoEmoji,
{ {
commandName += ' ' + words[i + 1]; commandName += ' ' + words[i + 1];
auto it = this->commandsMap_.find(commandName); const auto it = this->commandsMap_.find(commandName);
if (it != this->commandsMap_.end()) 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, QString CommandController::execCustomCommand(const QStringList &words,
const Command &command) const Command &command,
bool dryRun)
{ {
QString result; QString result;
@ -509,7 +581,17 @@ QString CommandController::execCustomCommand(const QStringList &words,
result = result.mid(1); result = result.mid(1);
} }
return result.replace("{{", "{"); auto res = result.replace("{{", "{");
if (dryRun || !appendWhisperMessageStringLocally(res))
{
return res;
}
else
{
sendWhisperMessage(res);
return "";
}
} }
QStringList CommandController::getDefaultTwitchCommandList() QStringList CommandController::getDefaultTwitchCommandList()

View file

@ -49,7 +49,8 @@ private:
std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>> std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>>
commandsSetting_; commandsSetting_;
QString execCustomCommand(const QStringList &words, const Command &command); QString execCustomCommand(const QStringList &words, const Command &command,
bool dryRun);
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -12,6 +12,8 @@ class Paths;
class IgnoreModel; class IgnoreModel;
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
class IgnoreController final : public Singleton class IgnoreController final : public Singleton
{ {
public: public:

View file

@ -73,6 +73,10 @@ ModerationAction::ModerationAction(const QString &action)
{ {
this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban); this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban);
} }
else if (action.startsWith("/delete"))
{
this->image_ = Image::fromPixmap(getApp()->resources->buttons.trashCan);
}
else else
{ {
QString xD = action; QString xD = action;

View file

@ -103,16 +103,12 @@ void NotificationController::playSound()
static auto player = new QMediaPlayer; static auto player = new QMediaPlayer;
static QUrl currentPlayerUrl; static QUrl currentPlayerUrl;
QUrl highlightSoundUrl; QUrl highlightSoundUrl =
if (getSettings()->notificationCustomSound) getSettings()->notificationCustomSound
{ ? QUrl::fromLocalFile(
highlightSoundUrl = QUrl::fromLocalFile( getSettings()->notificationPathSound.getValue())
getSettings()->notificationPathSound.getValue()); : QUrl("qrc:/sounds/ping2.wav");
}
else
{
highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav");
}
if (currentPlayerUrl != highlightSoundUrl) if (currentPlayerUrl != highlightSoundUrl)
{ {
player->setMedia(highlightSoundUrl); player->setMedia(highlightSoundUrl);

View file

@ -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<int>::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

View file

@ -0,0 +1,36 @@
#pragma once
#include <QObject>
#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<QString> channelVector;
ChatterinoSetting<std::vector<QString>> pingSetting_ = {"/pings/muted"};
};
} // namespace chatterino

View file

@ -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<QString>(1, parent)
{
}
// turn a vector item into a model row
QString PingModel::getItemFromRow(std::vector<QStandardItem *> &row,
const QString &original)
{
return QString(row[0]->data(Qt::DisplayRole).toString());
}
// turn a model
void PingModel::getRowFromItem(const QString &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item);
}
} // namespace chatterino

View file

@ -0,0 +1,28 @@
#pragma once
#include <QObject>
#include "common/SignalVectorModel.hpp"
#include "controllers/notifications/NotificationController.hpp"
namespace chatterino {
class PingController;
class PingModel : public SignalVectorModel<QString>
{
explicit PingModel(QObject *parent);
protected:
// turn a vector item into a model row
virtual QString getItemFromRow(std::vector<QStandardItem *> &row,
const QString &original) override;
// turns a row in the model into a vector item
virtual void getRowFromItem(const QString &item,
std::vector<QStandardItem *> &row) override;
friend class PingController;
};
} // namespace chatterino

View file

@ -1,10 +0,0 @@
#pragma once
namespace chatterino {
enum HistoricMessageAppearance {
Crossed = (1 << 0),
Greyed = (1 << 1),
};
} // namespace chatterino

View file

@ -28,8 +28,8 @@ template <typename T>
class LimitedQueue class LimitedQueue
{ {
protected: protected:
typedef std::shared_ptr<std::vector<T>> Chunk; using Chunk = std::vector<T>;
typedef std::shared_ptr<std::vector<Chunk>> ChunkVector; using ChunkVector = std::vector<std::shared_ptr<Chunk>>;
public: public:
LimitedQueue(size_t limit = 1000) LimitedQueue(size_t limit = 1000)
@ -42,9 +42,8 @@ public:
{ {
std::lock_guard<std::mutex> lock(this->mutex_); std::lock_guard<std::mutex> lock(this->mutex_);
this->chunks_ = this->chunks_ = std::make_shared<ChunkVector>();
std::make_shared<std::vector<std::shared_ptr<std::vector<T>>>>(); auto chunk = std::make_shared<Chunk>();
Chunk chunk = std::make_shared<std::vector<T>>();
chunk->resize(this->chunkSize_); chunk->resize(this->chunkSize_);
this->chunks_->push_back(chunk); this->chunks_->push_back(chunk);
this->firstChunkOffset_ = 0; this->firstChunkOffset_ = 0;
@ -57,23 +56,21 @@ public:
{ {
std::lock_guard<std::mutex> lock(this->mutex_); std::lock_guard<std::mutex> lock(this->mutex_);
Chunk lastChunk = this->chunks_->back(); auto lastChunk = this->chunks_->back();
// still space in the last chunk
if (lastChunk->size() <= this->lastChunkEnd_) if (lastChunk->size() <= this->lastChunkEnd_)
{ {
// create new chunk vector // Last chunk is full, create a new one and rebuild our chunk vector
ChunkVector newVector = std::make_shared< auto newVector = std::make_shared<ChunkVector>();
std::vector<std::shared_ptr<std::vector<T>>>>();
// copy chunks // copy chunks
for (Chunk &chunk : *this->chunks_) for (auto &chunk : *this->chunks_)
{ {
newVector->push_back(chunk); newVector->push_back(chunk);
} }
// push back new chunk // push back new chunk
Chunk newChunk = std::make_shared<std::vector<T>>(); auto newChunk = std::make_shared<Chunk>();
newChunk->resize(this->chunkSize_); newChunk->resize(this->chunkSize_);
newVector->push_back(newChunk); newVector->push_back(newChunk);
@ -98,8 +95,7 @@ public:
std::lock_guard<std::mutex> lock(this->mutex_); std::lock_guard<std::mutex> lock(this->mutex_);
// create new vector to clone chunks into // create new vector to clone chunks into
ChunkVector newChunks = std::make_shared< auto newChunks = std::make_shared<ChunkVector>();
std::vector<std::shared_ptr<std::vector<T>>>>();
newChunks->resize(this->chunks_->size()); newChunks->resize(this->chunks_->size());
@ -112,7 +108,7 @@ public:
// create new chunk for the first one // create new chunk for the first one
size_t offset = size_t offset =
std::min(this->space(), static_cast<qsizetype>(items.size())); std::min(this->space(), static_cast<qsizetype>(items.size()));
Chunk newFirstChunk = std::make_shared<std::vector<T>>(); auto newFirstChunk = std::make_shared<Chunk>();
newFirstChunk->resize(this->chunks_->front()->size() + offset); newFirstChunk->resize(this->chunks_->front()->size() + offset);
for (size_t i = 0; i < offset; i++) for (size_t i = 0; i < offset; i++)
@ -150,7 +146,7 @@ public:
for (size_t i = 0; i < this->chunks_->size(); i++) 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 start = i == 0 ? this->firstChunkOffset_ : 0;
size_t end = size_t end =
@ -160,7 +156,7 @@ public:
{ {
if (chunk->at(j) == item) if (chunk->at(j) == item)
{ {
Chunk newChunk = std::make_shared<std::vector<T>>(); auto newChunk = std::make_shared<Chunk>();
newChunk->resize(chunk->size()); newChunk->resize(chunk->size());
for (size_t k = 0; k < chunk->size(); k++) for (size_t k = 0; k < chunk->size(); k++)
@ -189,7 +185,7 @@ public:
for (size_t i = 0; i < this->chunks_->size(); i++) 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 start = i == 0 ? this->firstChunkOffset_ : 0;
size_t end = size_t end =
@ -199,7 +195,7 @@ public:
{ {
if (x == index) if (x == index)
{ {
Chunk newChunk = std::make_shared<std::vector<T>>(); auto newChunk = std::make_shared<Chunk>();
newChunk->resize(chunk->size()); newChunk->resize(chunk->size());
for (size_t k = 0; k < chunk->size(); k++) for (size_t k = 0; k < chunk->size(); k++)
@ -233,7 +229,7 @@ private:
qsizetype space() qsizetype space()
{ {
size_t totalSize = 0; size_t totalSize = 0;
for (Chunk &chunk : *this->chunks_) for (auto &chunk : *this->chunks_)
{ {
totalSize += chunk->size(); totalSize += chunk->size();
} }
@ -257,18 +253,15 @@ private:
deleted = this->chunks_->front()->at(this->firstChunkOffset_); deleted = this->chunks_->front()->at(this->firstChunkOffset_);
this->firstChunkOffset_++;
// need to delete the first chunk // need to delete the first chunk
if (this->firstChunkOffset_ == this->chunks_->front()->size() - 1) if (this->firstChunkOffset_ == this->chunks_->front()->size() - 1)
{ {
// copy the chunk vector // copy the chunk vector
ChunkVector newVector = std::make_shared< auto newVector = std::make_shared<ChunkVector>();
std::vector<std::shared_ptr<std::vector<T>>>>();
// delete first chunk // delete first chunk
bool first = true; bool first = true;
for (Chunk &chunk : *this->chunks_) for (auto &chunk : *this->chunks_)
{ {
if (!first) if (!first)
{ {
@ -280,15 +273,20 @@ private:
this->chunks_ = newVector; this->chunks_ = newVector;
this->firstChunkOffset_ = 0; this->firstChunkOffset_ = 0;
} }
else
{
this->firstChunkOffset_++;
}
return true; return true;
} }
ChunkVector chunks_; std::shared_ptr<ChunkVector> chunks_;
std::mutex mutex_; std::mutex mutex_;
size_t firstChunkOffset_; size_t firstChunkOffset_;
size_t lastChunkEnd_; size_t lastChunkEnd_;
size_t limit_; const size_t limit_;
const size_t chunkSize_ = 100; const size_t chunkSize_ = 100;
}; };

View file

@ -12,7 +12,7 @@
namespace chatterino { namespace chatterino {
class MessageElement; class MessageElement;
enum class MessageFlag : uint16_t { enum class MessageFlag : uint32_t {
None = 0, None = 0,
System = (1 << 0), System = (1 << 0),
Timeout = (1 << 1), Timeout = (1 << 1),
@ -30,6 +30,7 @@ enum class MessageFlag : uint16_t {
DoNotLog = (1 << 13), DoNotLog = (1 << 13),
AutoMod = (1 << 14), AutoMod = (1 << 14),
RecentMessage = (1 << 15), RecentMessage = (1 << 15),
Whisper = (1 << 16)
}; };
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;
@ -47,6 +48,7 @@ struct Message : boost::noncopyable {
QTime parseTime; QTime parseTime;
QString id; QString id;
QString searchText; QString searchText;
QString messageText;
QString loginName; QString loginName;
QString displayName; QString displayName;
QString localizedName; QString localizedName;

View file

@ -5,6 +5,7 @@
#include "messages/Image.hpp" #include "messages/Image.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/LinkResolver.hpp"
#include "providers/twitch/PubsubActions.hpp" #include "providers/twitch/PubsubActions.hpp"
#include "singletons/Emotes.hpp" #include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp" #include "singletons/Resources.hpp"
@ -64,6 +65,8 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
builder = MessageBuilder(); builder = MessageBuilder();
builder.emplace<TimestampElement>(); builder.emplace<TimestampElement>();
builder.emplace<TwitchModerationElement>();
builder.message().loginName = action.target.name;
builder.message().flags.set(MessageFlag::PubSub); builder.message().flags.set(MessageFlag::PubSub);
builder builder
@ -90,23 +93,30 @@ MessageBuilder::MessageBuilder()
{ {
} }
MessageBuilder::MessageBuilder(const QString &text)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
this->message().searchText = text;
}
MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text) MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
: MessageBuilder() : MessageBuilder()
{ {
this->emplace<TimestampElement>(); this->emplace<TimestampElement>();
this->emplace<TextElement>(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<TextElement>(word, MessageElementFlag::Text,
MessageColor::System);
}
else
{
this->addLink(word, linkString);
}
}
this->message().flags.set(MessageFlag::System); this->message().flags.set(MessageFlag::System);
this->message().flags.set(MessageFlag::DoNotTriggerNotification); this->message().flags.set(MessageFlag::DoNotTriggerNotification);
this->message().messageText = text;
this->message().searchText = text; this->message().searchText = text;
} }
@ -157,6 +167,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
this->emplace<TimestampElement>(); this->emplace<TimestampElement>();
this->emplace<TextElement>(text, MessageElementFlag::Text, this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System); MessageColor::System);
this->message().messageText = text;
this->message().searchText = text; this->message().searchText = text;
} }
@ -213,6 +224,7 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
this->emplace<TextElement>(text, MessageElementFlag::Text, this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System); MessageColor::System);
this->message().messageText = text;
this->message().searchText = text; this->message().searchText = text;
} }
@ -225,23 +237,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
this->message().timeoutUser = action.target.name; this->message().timeoutUser = action.target.name;
QString text; QString text =
QString("%1 %2 %3.")
if (action.wasBan()) .arg(action.source.name)
{ .arg(QString(action.wasBan() ? "unbanned" : "untimedout"))
text = QString("%1 unbanned %2.") // .arg(action.target.name);
.arg(action.source.name)
.arg(action.target.name);
}
else
{
text = QString("%1 untimedout %2.") //
.arg(action.source.name)
.arg(action.target.name);
}
this->emplace<TextElement>(text, MessageElementFlag::Text, this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System); MessageColor::System);
this->message().messageText = text;
this->message().searchText = text; this->message().searchText = text;
} }
@ -352,4 +356,57 @@ QString MessageBuilder::matchLink(const QString &string)
return captured; 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<TextElement>(lowercaseLinkString,
MessageElementFlag::LowercaseLink, textColor)
->setLink(linkElement);
auto linkMEOriginal =
this->emplace<TextElement>(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 } // namespace chatterino

View file

@ -38,7 +38,6 @@ class MessageBuilder
{ {
public: public:
MessageBuilder(); MessageBuilder();
MessageBuilder(const QString &text);
MessageBuilder(SystemMessageTag, const QString &text); MessageBuilder(SystemMessageTag, const QString &text);
MessageBuilder(TimeoutMessageTag, const QString &username, MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds, const QString &reason, const QString &durationInSeconds, const QString &reason,
@ -54,6 +53,7 @@ public:
void append(std::unique_ptr<MessageElement> element); void append(std::unique_ptr<MessageElement> element);
QString matchLink(const QString &string); QString matchLink(const QString &string);
void addLink(const QString &origLink, const QString &matchedLink);
template <typename T, typename... Args> template <typename T, typename... Args>
T *emplace(Args &&... args) T *emplace(Args &&... args)

View file

@ -138,10 +138,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
if (image->isEmpty()) if (image->isEmpty())
return; return;
auto emoteScale = auto emoteScale = getSettings()->emoteScale.getValue();
this->getFlags().hasAny(MessageElementFlag::Badges)
? 1
: getSettings()->emoteScale.getValue();
auto size = auto size =
QSize(int(container.getScale() * image->width() * emoteScale), 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 // TEXT
TextElement::TextElement(const QString &text, MessageElementFlags flags, TextElement::TextElement(const QString &text, MessageElementFlags flags,
const MessageColor &color, FontStyle style) const MessageColor &color, FontStyle style)
@ -188,7 +210,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
for (Word &word : this->words_) for (Word &word : this->words_)
{ {
auto getTextLayoutElement = [&](QString text, int width, auto getTextLayoutElement = [&](QString text, int width,
bool trailingSpace) { bool hasTrailingSpace) {
auto color = this->color_.getColor(*app->themes); auto color = this->color_.getColor(*app->themes);
app->themes->normalizeColor(color); app->themes->normalizeColor(color);
@ -196,7 +218,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
*this, text, QSize(width, metrics.height()), *this, text, QSize(width, metrics.height()),
color, this->style_, container.getScale())) color, this->style_, container.getScale()))
->setLink(this->getLink()); ->setLink(this->getLink());
e->setTrailingSpace(trailingSpace); e->setTrailingSpace(hasTrailingSpace);
e->setText(text); e->setText(text);
// If URL link was changed, // If URL link was changed,
@ -291,7 +313,6 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container,
{ {
if (flags.hasAny(this->getFlags())) if (flags.hasAny(this->getFlags()))
{ {
auto app = getApp();
if (getSettings()->timestampFormat != this->format_) if (getSettings()->timestampFormat != this->format_)
{ {
this->format_ = getSettings()->timestampFormat.getValue(); this->format_ = getSettings()->timestampFormat.getValue();

View file

@ -42,6 +42,7 @@ enum class MessageElementFlag {
FfzEmoteText = (1 << 11), FfzEmoteText = (1 << 11),
FfzEmote = FfzEmoteImage | FfzEmoteText, FfzEmote = FfzEmoteImage | FfzEmoteText,
EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage, EmoteImages = TwitchEmoteImage | BttvEmoteImage | FfzEmoteImage,
EmoteText = TwitchEmoteText | BttvEmoteText | FfzEmoteText,
BitsStatic = (1 << 12), BitsStatic = (1 << 12),
BitsAnimated = (1 << 13), BitsAnimated = (1 << 13),
@ -74,9 +75,6 @@ enum class MessageElementFlag {
// - Chatterino top donator badge // - Chatterino top donator badge
BadgeChatterino = (1 << 18), BadgeChatterino = (1 << 18),
// Rest of slots: ffz custom badge? bttv custom badge? mywaifu (puke)
// custom badge?
Badges = BadgeGlobalAuthority | BadgeChannelAuthority | BadgeSubscription | Badges = BadgeGlobalAuthority | BadgeChannelAuthority | BadgeSubscription |
BadgeVanity | BadgeChatterino, BadgeVanity | BadgeChatterino,
@ -216,6 +214,18 @@ private:
EmotePtr emote_; 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 // contains a text, formated depending on the preferences
class TimestampElement : public MessageElement class TimestampElement : public MessageElement
{ {

View file

@ -99,17 +99,14 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags)
return true; return true;
} }
void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags) void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
{ {
this->layoutCount_++; this->layoutCount_++;
const auto addTest = this->message_->flags.hasAny(
{MessageFlag::DisconnectedMessage, MessageFlag::ConnectedMessage});
auto messageFlags = this->message_->flags; auto messageFlags = this->message_->flags;
if (this->flags.has(MessageLayoutFlag::Expanded) || if (this->flags.has(MessageLayoutFlag::Expanded) ||
(_flags.has(MessageElementFlag::ModeratorTools) && (flags.has(MessageElementFlag::ModeratorTools) &&
!this->message_->flags.has(MessageFlag::Disabled))) // !this->message_->flags.has(MessageFlag::Disabled))) //
{ {
messageFlags.unset(MessageFlag::Collapsed); messageFlags.unset(MessageFlag::Collapsed);
@ -117,25 +114,21 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags _flags)
this->container_->begin(width, this->scale_, messageFlags); 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) for (const auto &element : this->message_->elements)
{ {
element->addToContainer(*this->container_, _flags); if (getSettings()->hideModerated &&
} this->message_->flags.has(MessageFlag::Disabled))
{
continue;
}
if (addTest) if (getSettings()->hideModerationActions &&
{ this->message_->flags.has(MessageFlag::Timeout))
this->container_->breakLine(); {
this->container_->addElement(new TestLayoutElement( continue;
EmptyElement::instance(), QSize(width, this->scale_ * 6), }
getTheme()->messages.backgrounds.regular, true));
element->addToContainer(*this->container_, flags);
} }
if (this->height_ != this->container_->getHeight()) 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); app->themes->messages.disabled);
// painter.fillRect(0, y, pixmap->width(), pixmap->height(), // painter.fillRect(0, y, pixmap->width(), pixmap->height(),
// QBrush(QColor(64, 64, 64, 64))); // 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)) if (this->message_->flags.has(MessageFlag::RecentMessage))
{ {
const auto &historicMessageAppearance = painter.fillRect(0, y, pixmap->width(), pixmap->height(),
getSettings()->historicMessagesAppearance.getValue(); app->themes->messages.disabled);
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);
}
} }
// draw selection // draw selection

View file

@ -157,7 +157,7 @@ void MessageLayoutContainer::breakLine()
int yExtra = 0; int yExtra = 0;
if (isCompactEmote) if (isCompactEmote)
{ {
// yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_; yExtra = (COMPACT_EMOTES_OFFSET / 2) * this->scale_;
} }
// if (element->getCreator().getFlags() & // if (element->getCreator().getFlags() &
@ -390,17 +390,17 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
int lineIndex2 = lineIndex + 1; int lineIndex2 = lineIndex + 1;
for (; lineIndex2 < this->lines_.size(); lineIndex2++) for (; lineIndex2 < this->lines_.size(); lineIndex2++)
{ {
Line &line = this->lines_[lineIndex2]; Line &line2 = this->lines_[lineIndex2];
QRect rect = line.rect; QRect rect = line2.rect;
rect.setTop(std::max(0, rect.top()) + yOffset); rect.setTop(std::max(0, rect.top()) + yOffset);
rect.setBottom( rect.setBottom(
std::min(this->height_, rect.bottom()) + std::min(this->height_, rect.bottom()) +
yOffset); yOffset);
rect.setLeft(this->elements_[line.startIndex] rect.setLeft(this->elements_[line2.startIndex]
->getRect() ->getRect()
.left()); .left());
rect.setRight(this->elements_[line.endIndex - 1] rect.setRight(this->elements_[line2.endIndex - 1]
->getRect() ->getRect()
.right()); .right());

View file

@ -14,7 +14,7 @@ class QPainter;
namespace chatterino { namespace chatterino {
enum class MessageFlag : uint16_t; enum class MessageFlag : uint32_t;
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;
struct Margin { struct Margin {

View file

@ -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 } // namespace chatterino

View file

@ -41,6 +41,7 @@ public:
virtual void paintAnimated(QPainter &painter, int yOffset) = 0; virtual void paintAnimated(QPainter &painter, int yOffset) = 0;
virtual int getMouseOverIndex(const QPoint &abs) const = 0; virtual int getMouseOverIndex(const QPoint &abs) const = 0;
virtual int getXFromIndex(int index) = 0; virtual int getXFromIndex(int index) = 0;
const Link &getLink() const; const Link &getLink() const;
const QString &getText() const; const QString &getText() const;
FlagsEnum<MessageElementFlag> getFlags() const; FlagsEnum<MessageElementFlag> getFlags() const;
@ -125,25 +126,4 @@ private:
QString line2; 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 } // namespace chatterino

View file

@ -1,6 +1,7 @@
#include "providers/LinkResolver.hpp" #include "providers/LinkResolver.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "messages/Link.hpp" #include "messages/Link.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
@ -12,12 +13,15 @@ namespace chatterino {
void LinkResolver::getLinkInfo( void LinkResolver::getLinkInfo(
const QString url, std::function<void(QString, Link)> successCallback) const QString url, std::function<void(QString, Link)> successCallback)
{ {
QString requestUrl("https://braize.pajlada.com/chatterino/link_resolver/" + if (!getSettings()->linkInfoTooltip)
QUrl::toPercentEncoding(url, "", "/:")); {
successCallback("No link info loaded", Link(Link::Url, url));
return;
}
// Uncomment to test crashes // Uncomment to test crashes
// QTimer::singleShot(3000, [=]() { // QTimer::singleShot(3000, [=]() {
NetworkRequest request(requestUrl); NetworkRequest request(Env::get().linkResolverUrl.arg(
QString::fromUtf8(QUrl::toPercentEncoding(url, "", "/:"))));
request.setCaller(QThread::currentThread()); request.setCaller(QThread::currentThread());
request.setTimeout(30000); request.setTimeout(30000);
request.onSuccess([successCallback, url](auto result) mutable -> Outcome { request.onSuccess([successCallback, url](auto result) mutable -> Outcome {

View file

@ -36,9 +36,9 @@ namespace {
else else
{ {
const auto &shortCodes = unparsedEmoji["short_names"]; 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() void Emojis::loadEmojiSet()
{ {
auto app = getApp();
getSettings()->emojiSet.connect([=](const auto &emojiSet) { getSettings()->emojiSet.connect([=](const auto &emojiSet) {
this->emojis.each([=](const auto &name, this->emojis.each([=](const auto &name,
std::shared_ptr<EmojiData> &emoji) { std::shared_ptr<EmojiData> &emoji) {

View file

@ -231,9 +231,8 @@ void AbstractIrcServer::onConnected()
{ {
std::lock_guard<std::mutex> lock(this->channelMutex); std::lock_guard<std::mutex> lock(this->channelMutex);
auto connected = makeSystemMessage("connected"); auto connectedMsg = makeSystemMessage("connected");
connected->flags.set(MessageFlag::ConnectedMessage); connectedMsg->flags.set(MessageFlag::ConnectedMessage);
connected->flags.set(MessageFlag::Centered);
auto reconnected = makeSystemMessage("reconnected"); auto reconnected = makeSystemMessage("reconnected");
reconnected->flags.set(MessageFlag::ConnectedMessage); reconnected->flags.set(MessageFlag::ConnectedMessage);
@ -257,7 +256,7 @@ void AbstractIrcServer::onConnected()
continue; continue;
} }
chan->addMessage(connected); chan->addMessage(connectedMsg);
} }
this->falloffCounter_ = 1; this->falloffCounter_ = 1;
@ -269,7 +268,7 @@ void AbstractIrcServer::onDisconnected()
MessageBuilder b(systemMessage, "disconnected"); MessageBuilder b(systemMessage, "disconnected");
b->flags.set(MessageFlag::DisconnectedMessage); b->flags.set(MessageFlag::DisconnectedMessage);
auto disconnected = b.release(); auto disconnectedMsg = b.release();
for (std::weak_ptr<Channel> &weak : this->channels.values()) for (std::weak_ptr<Channel> &weak : this->channels.values())
{ {
@ -279,7 +278,7 @@ void AbstractIrcServer::onDisconnected()
continue; continue;
} }
chan->addMessage(disconnected); chan->addMessage(disconnectedMsg);
} }
} }

View file

@ -20,12 +20,69 @@
namespace chatterino { namespace chatterino {
static QMap<QString, QString> parseBadges(QString badgesString)
{
QMap<QString, QString> 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() IrcMessageHandler &IrcMessageHandler::getInstance()
{ {
static IrcMessageHandler instance; static IrcMessageHandler instance;
return instance; return instance;
} }
std::vector<MessagePtr> IrcMessageHandler::parseMessage(
Channel *channel, Communi::IrcMessage *message)
{
std::vector<MessagePtr> builtMessages;
auto command = message->command();
if (command == "PRIVMSG")
{
return this->parsePrivMessage(
channel, static_cast<Communi::IrcPrivateMessage *>(message));
}
else if (command == "USERNOTICE")
{
return this->parseUserNoticeMessage(channel, message);
}
else if (command == "NOTICE")
{
return this->parseNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
return builtMessages;
}
std::vector<MessagePtr> IrcMessageHandler::parsePrivMessage(
Channel *channel, Communi::IrcPrivateMessage *message)
{
std::vector<MessagePtr> 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, void IrcMessageHandler::handlePrivMessage(Communi::IrcPrivateMessage *message,
TwitchServer &server) TwitchServer &server)
{ {
@ -203,28 +260,78 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
// refresh all // refresh all
app->windows->repaintVisibleChatWidgets(chan.get()); 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) 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<TwitchChannel *>(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()) 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<TwitchChannel *>(c.get()); TwitchChannel *tc = dynamic_cast<TwitchChannel *>(c.get());
if (tc != nullptr) if (tc != nullptr)
{ {
@ -248,6 +355,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
if (!builder.isIgnored()) if (!builder.isIgnored())
{ {
builder->flags.set(MessageFlag::Whisper);
MessagePtr _message = builder.build(); MessagePtr _message = builder.build();
app->twitch.server->lastUserThatWhisperedMe.set(builder.userName); app->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
@ -273,6 +381,56 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
} }
} }
std::vector<MessagePtr> IrcMessageHandler::parseUserNoticeMessage(
Channel *channel, Communi::IrcMessage *message)
{
std::vector<MessagePtr> 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, void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
TwitchServer &server) TwitchServer &server)
{ {
@ -352,35 +510,60 @@ void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message)
} }
} }
std::vector<MessagePtr> IrcMessageHandler::parseNoticeMessage(
Communi::IrcNoticeMessage *message)
{
std::vector<MessagePtr> builtMessages;
builtMessages.emplace_back(makeSystemMessage(message->content()));
return builtMessages;
}
void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message) void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
{ {
auto app = getApp(); auto app = getApp();
MessagePtr msg = makeSystemMessage(message->content()); auto builtMessages = this->parseNoticeMessage(message);
QString channelName; for (auto msg : builtMessages)
if (!trimChannelName(message->target(), channelName))
{ {
// Notice wasn't targeted at a single channel, send to all twitch QString channelName;
// channels if (!trimChannelName(message->target(), channelName) ||
app->twitch.server->forEachChannelAndSpecialChannels( channelName == "jtv")
[msg](const auto &c) { {
c->addMessage(msg); // // Notice wasn't targeted at a single channel, send to all twitch
}); // channels
app->twitch.server->forEachChannelAndSpecialChannels(
[msg](const auto &c) {
c->addMessage(msg); //
});
return; 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 <msg-id>\" - 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( void IrcMessageHandler::handleWriteConnectionNoticeMessage(

View file

@ -1,10 +1,12 @@
#pragma once #pragma once
#include <IrcMessage> #include <IrcMessage>
#include "messages/Message.hpp"
namespace chatterino { namespace chatterino {
class TwitchServer; class TwitchServer;
class Channel;
class IrcMessageHandler class IrcMessageHandler
{ {
@ -13,17 +15,37 @@ class IrcMessageHandler
public: public:
static IrcMessageHandler &getInstance(); static IrcMessageHandler &getInstance();
// parseMessage parses a single IRC message into 0+ Chatterino messages
std::vector<MessagePtr> parseMessage(Channel *channel,
Communi::IrcMessage *message);
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
std::vector<MessagePtr> parsePrivMessage(
Channel *channel, Communi::IrcPrivateMessage *message);
void handlePrivMessage(Communi::IrcPrivateMessage *message, void handlePrivMessage(Communi::IrcPrivateMessage *message,
TwitchServer &server); TwitchServer &server);
void handleRoomStateMessage(Communi::IrcMessage *message); void handleRoomStateMessage(Communi::IrcMessage *message);
void handleClearChatMessage(Communi::IrcMessage *message); void handleClearChatMessage(Communi::IrcMessage *message);
void handleClearMessageMessage(Communi::IrcMessage *message);
void handleUserStateMessage(Communi::IrcMessage *message); void handleUserStateMessage(Communi::IrcMessage *message);
void handleWhisperMessage(Communi::IrcMessage *message); void handleWhisperMessage(Communi::IrcMessage *message);
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+
// chatterino messages
std::vector<MessagePtr> parseUserNoticeMessage(
Channel *channel, Communi::IrcMessage *message);
void handleUserNoticeMessage(Communi::IrcMessage *message, void handleUserNoticeMessage(Communi::IrcMessage *message,
TwitchServer &server); TwitchServer &server);
void handleModeMessage(Communi::IrcMessage *message); void handleModeMessage(Communi::IrcMessage *message);
// parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino
// messages
std::vector<MessagePtr> parseNoticeMessage(
Communi::IrcNoticeMessage *message);
void handleNoticeMessage(Communi::IrcNoticeMessage *message); void handleNoticeMessage(Communi::IrcNoticeMessage *message);
void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message); void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message);
void handleJoinMessage(Communi::IrcMessage *message); void handleJoinMessage(Communi::IrcMessage *message);

View file

@ -3,6 +3,7 @@
#include <QThread> #include <QThread>
#include "Application.hpp" #include "Application.hpp"
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
@ -534,9 +535,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
return; return;
} }
NetworkRequest req( NetworkRequest req(Env::get().twitchEmoteSetResolverUrl.arg(emoteSet->key));
"https://braize.pajlada.com/chatterino/twitchemotes/set/" +
emoteSet->key + "/");
req.setUseQuickLoadCache(true); req.setUseQuickLoadCache(true);
req.onError([](int errorCode) -> bool { req.onError([](int errorCode) -> bool {

View file

@ -172,8 +172,6 @@ bool TwitchAccountManager::isLoggedIn() const
bool TwitchAccountManager::removeUser(TwitchAccount *account) bool TwitchAccountManager::removeUser(TwitchAccount *account)
{ {
const auto &accs = this->accounts.getVector();
auto userID(account->getUserId()); auto userID(account->getUserId());
if (!userID.isEmpty()) if (!userID.isEmpty())
{ {

View file

@ -2,6 +2,7 @@
#include "Application.hpp" #include "Application.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp" #include "common/NetworkRequest.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
@ -9,6 +10,7 @@
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/LoadBttvChannelEmote.hpp" #include "providers/bttv/LoadBttvChannelEmote.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/PubsubClient.hpp"
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
@ -29,10 +31,14 @@
namespace chatterino { namespace chatterino {
namespace { 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) auto parseRecentMessages(const QJsonObject &jsonRoot, ChannelPtr channel)
{ {
QJsonArray jsonMessages = jsonRoot.value("messages").toArray(); QJsonArray jsonMessages = jsonRoot.value("messages").toArray();
std::vector<MessagePtr> messages; std::vector<Communi::IrcMessage *> messages;
if (jsonMessages.empty()) if (jsonMessages.empty())
return messages; return messages;
@ -40,26 +46,17 @@ namespace {
for (const auto jsonMessage : jsonMessages) for (const auto jsonMessage : jsonMessages)
{ {
auto content = jsonMessage.toString().toUtf8(); auto content = jsonMessage.toString().toUtf8();
// passing nullptr as the channel makes the message invalid but we messages.emplace_back(
// don't check for that anyways Communi::IrcMessage::fromData(content, nullptr));
auto message = Communi::IrcMessage::fromData(content, nullptr);
auto privMsg = dynamic_cast<Communi::IrcPrivateMessage *>(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());
} }
return messages; return messages;
} }
std::pair<Outcome, UsernameSet> parseChatters(const QJsonObject &jsonRoot) std::pair<Outcome, UsernameSet> parseChatters(const QJsonObject &jsonRoot)
{ {
static QStringList categories = {"moderators", "staff", "admins", static QStringList categories = {"broadcaster", "vips", "moderators",
"global_mods", "viewers"}; "staff", "admins", "global_mods",
"viewers"};
auto usernames = UsernameSet(); auto usernames = UsernameSet();
@ -127,10 +124,6 @@ TwitchChannel::TwitchChannel(const QString &name,
[=] { this->refreshLiveStatus(); }); [=] { this->refreshLiveStatus(); });
this->liveStatusTimer_.start(60 * 1000); this->liveStatusTimer_.start(60 * 1000);
// --
this->messageSuffix_.append(' ');
this->messageSuffix_.append(QChar(0x206D));
// debugging // debugging
#if 0 #if 0
for (int i = 0; i < 1000; i++) { for (int i = 0; i < 1000; i++) {
@ -199,13 +192,13 @@ void TwitchChannel::sendMessage(const QString &message)
return; return;
} }
if (!this->hasModRights()) if (!this->hasHighRateLimit())
{ {
if (getSettings()->allowDuplicateMessages) if (getSettings()->allowDuplicateMessages)
{ {
if (parsedMessage == this->lastSentMessage_) if (parsedMessage == this->lastSentMessage_)
{ {
parsedMessage.append(this->messageSuffix_); parsedMessage.append(MAGIC_MESSAGE_SUFFIX);
} }
} }
} }
@ -225,6 +218,16 @@ bool TwitchChannel::isMod() const
return this->mod_; return this->mod_;
} }
bool TwitchChannel::isVIP() const
{
return this->vip_;
}
bool TwitchChannel::isStaff() const
{
return this->staff_;
}
void TwitchChannel::setMod(bool value) void TwitchChannel::setMod(bool value)
{ {
if (this->mod_ != 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 bool TwitchChannel::isBroadcaster() const
{ {
auto app = getApp(); auto app = getApp();
@ -242,6 +265,11 @@ bool TwitchChannel::isBroadcaster() const
return this->getName() == app->accounts->twitch.getCurrent()->getUserName(); 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) void TwitchChannel::addRecentChatter(const MessagePtr &message)
{ {
this->chatters_.access()->insert(message->displayName); this->chatters_.access()->insert(message->displayName);
@ -445,7 +473,7 @@ void TwitchChannel::setLive(bool newLiveStatus)
else else
{ {
auto offline = auto offline =
makeSystemMessage(this->getName() + " is offline"); makeSystemMessage(this->getDisplayName() + " is offline");
this->addMessage(offline); this->addMessage(offline);
} }
guard->live = newLiveStatus; guard->live = newLiveStatus;
@ -575,12 +603,13 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
void TwitchChannel::loadRecentMessages() void TwitchChannel::loadRecentMessages()
{ {
static QString genericURL = if (!getSettings()->loadTwitchMessageHistoryOnConnect)
"https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + {
getDefaultClientID(); return;
}
NetworkRequest request(genericURL.arg(this->roomId())); NetworkRequest request(
request.makeAuthorizedV5(getDefaultClientID()); Env::get().recentMessagesApiUrl.arg(this->getName()));
request.setCaller(QThread::currentThread()); request.setCaller(QThread::currentThread());
// can't be concurrent right now due to SignalVector // can't be concurrent right now due to SignalVector
// request.setExecuteConcurrently(true); // request.setExecuteConcurrently(true);
@ -592,7 +621,21 @@ void TwitchChannel::loadRecentMessages()
auto messages = parseRecentMessages(result.parseJson(), shared); auto messages = parseRecentMessages(result.parseJson(), shared);
shared->addMessagesAtStart(messages); auto &handler = IrcMessageHandler::getInstance();
std::vector<MessagePtr> 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; return Success;
}); });
@ -618,11 +661,11 @@ void TwitchChannel::refreshChatters()
{ {
// setting? // setting?
const auto streamStatus = this->accessStreamStatus(); const auto streamStatus = this->accessStreamStatus();
const auto viewerCount = static_cast<int>(streamStatus->viewerCount);
if (getSettings()->onlyFetchChattersForSmallerStreamers) if (getSettings()->onlyFetchChattersForSmallerStreamers)
{ {
if (streamStatus->live && if (streamStatus->live &&
streamStatus->viewerCount > getSettings()->smallStreamerLimit) viewerCount > getSettings()->smallStreamerLimit)
{ {
return; return;
} }
@ -674,8 +717,8 @@ void TwitchChannel::refreshBadges()
{ {
auto &versions = (*badgeSets)[jsonBadgeSet.key()]; auto &versions = (*badgeSets)[jsonBadgeSet.key()];
auto _ = jsonBadgeSet->toObject()["versions"].toObject(); auto _set = jsonBadgeSet->toObject()["versions"].toObject();
for (auto jsonVersion_ = _.begin(); jsonVersion_ != _.end(); for (auto jsonVersion_ = _set.begin(); jsonVersion_ != _set.end();
jsonVersion_++) jsonVersion_++)
{ {
auto jsonVersion = jsonVersion_->toObject(); auto jsonVersion = jsonVersion_->toObject();

View file

@ -61,7 +61,10 @@ public:
virtual bool canSendMessage() const override; virtual bool canSendMessage() const override;
virtual void sendMessage(const QString &message) override; virtual void sendMessage(const QString &message) override;
virtual bool isMod() const override; virtual bool isMod() const override;
bool isVIP() const;
bool isStaff() const;
virtual bool isBroadcaster() const override; virtual bool isBroadcaster() const override;
virtual bool hasHighRateLimit() const override;
// Data // Data
const QString &subscriptionUrl(); const QString &subscriptionUrl();
@ -123,6 +126,8 @@ private:
void addPartedUser(const QString &user); void addPartedUser(const QString &user);
void setLive(bool newLiveStatus); void setLive(bool newLiveStatus);
void setMod(bool value); void setMod(bool value);
void setVIP(bool value);
void setStaff(bool value);
void setRoomId(const QString &id); void setRoomId(const QString &id);
void setRoomModes(const RoomModes &roomModes_); void setRoomModes(const RoomModes &roomModes_);
@ -151,6 +156,8 @@ private:
FfzModBadge ffzCustomModBadge_; FfzModBadge ffzCustomModBadge_;
bool mod_ = false; bool mod_ = false;
bool vip_ = false;
bool staff_ = false;
UniqueAccess<QString> roomID_; UniqueAccess<QString> roomID_;
UniqueAccess<QStringList> joinedUsers_; UniqueAccess<QStringList> joinedUsers_;
@ -159,7 +166,6 @@ private:
bool partedUsersMergeQueued_ = false; bool partedUsersMergeQueued_ = false;
// -- // --
QByteArray messageSuffix_;
QString lastSentMessage_; QString lastSentMessage_;
QObject lifetimeGuard_; QObject lifetimeGuard_;
QTimer liveStatusTimer_; QTimer liveStatusTimer_;

View file

@ -4,9 +4,9 @@
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp" #include "controllers/highlights/HighlightController.hpp"
#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnoreController.hpp"
#include "controllers/pings/PingController.hpp"
#include "debug/Log.hpp" #include "debug/Log.hpp"
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "providers/LinkResolver.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp" #include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
@ -80,6 +80,19 @@ bool TwitchMessageBuilder::isIgnored() const
{ {
if (sourceUserID == user.id) if (sourceUserID == user.id)
{ {
switch (static_cast<ShowIgnoredUsersMessages>(
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 {}", log("Blocking message because it's from blocked user {}",
user.name); user.name);
return true; return true;
@ -109,12 +122,24 @@ MessagePtr TwitchMessageBuilder::build()
this->appendChannelName(); this->appendChannelName();
if (this->tags.contains("rm-deleted"))
{
this->message().flags.set(MessageFlag::Disabled);
}
// timestamp // timestamp
bool isPastMsg = this->tags.contains("historical"); bool isPastMsg = this->tags.contains("historical");
if (isPastMsg) if (isPastMsg)
{ {
// This may be architecture dependent(datatype) // 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); QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(ts);
this->emplace<TimestampElement>(dateTime.time()); this->emplace<TimestampElement>(dateTime.time());
} }
@ -310,13 +335,13 @@ MessagePtr TwitchMessageBuilder::build()
QRegularExpression emoteregex( QRegularExpression emoteregex(
"\\b" + std::get<2>(tup).string + "\\b", "\\b" + std::get<2>(tup).string + "\\b",
QRegularExpression::UseUnicodePropertiesOption); QRegularExpression::UseUnicodePropertiesOption);
auto match = emoteregex.match(midExtendedRef); auto _match = emoteregex.match(midExtendedRef);
if (match.hasMatch()) if (_match.hasMatch())
{ {
int last = match.lastCapturedIndex(); int last = _match.lastCapturedIndex();
for (int i = 0; i <= last; ++i) 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)); twitchEmotes.push_back(std::move(tup));
} }
} }
@ -414,7 +439,9 @@ MessagePtr TwitchMessageBuilder::build()
this->addWords(splits, twitchEmotes); 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(); return this->release();
} }
@ -512,56 +539,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
} }
else else
{ {
static QRegularExpression domainRegex( this->addLink(string, linkString);
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<TextElement>(lowercaseLinkString,
MessageElementFlag::LowercaseLink,
textColor)
->setLink(link);
auto linkMEOriginal =
this->emplace<TextElement>(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();
}
});
} }
// if (!linkString.isEmpty()) { // if (!linkString.isEmpty()) {
@ -605,7 +583,7 @@ void TwitchMessageBuilder::parseMessageID()
if (iterator != this->tags.end()) 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() void TwitchMessageBuilder::appendChannelName()
{ {
QString channelName("#" + this->channel->getName()); 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<TextElement>(channelName, MessageElementFlag::ChannelName, this->emplace<TextElement>(channelName, MessageElementFlag::ChannelName,
MessageColor::System) // MessageColor::System) //
@ -805,16 +783,10 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
} }
// update the media player url if necessary // update the media player url if necessary
QUrl highlightSoundUrl; QUrl highlightSoundUrl =
if (getSettings()->customHighlightSound) getSettings()->customHighlightSound
{ ? QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue())
highlightSoundUrl = : QUrl("qrc:/sounds/ping2.wav");
QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue());
}
else
{
highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav");
}
if (currentPlayerUrl != highlightSoundUrl) if (currentPlayerUrl != highlightSoundUrl)
{ {
@ -916,13 +888,16 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
if (!isPastMsg) if (!isPastMsg)
{ {
if (playSound && bool notMuted = !getApp()->pings->isMuted(this->channel->getName());
(!hasFocus || getSettings()->highlightAlwaysPlaySound)) bool resolveFocus =
!hasFocus || getSettings()->highlightAlwaysPlaySound;
if (playSound && notMuted && resolveFocus)
{ {
player->play(); player->play();
} }
if (doAlert) if (doAlert && notMuted)
{ {
getApp()->windows->sendAlert(); getApp()->windows->sendAlert();
} }
@ -983,52 +958,36 @@ void TwitchMessageBuilder::appendTwitchEmote(
Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
{ {
// Special channels, like /whispers and /channels return here auto *app = getApp();
// This means they will not render any BTTV or FFZ emotes
if (this->twitchChannel == nullptr) const auto &globalBttvEmotes = app->twitch.server->getBttvEmotes();
{ const auto &globalFfzEmotes = app->twitch.server->getFfzEmotes();
auto *app = getApp();
const auto &bttvemotes = app->twitch.server->getBttvEmotes();
const auto &ffzemotes = app->twitch.server->getFfzEmotes();
auto flags = MessageElementFlags();
auto emote = boost::optional<EmotePtr>{};
{ // bttv/ffz emote
if ((emote = bttvemotes.emote(name)))
{
flags = MessageElementFlag::BttvEmote;
}
else if ((emote = ffzemotes.emote(name)))
{
flags = MessageElementFlag::FfzEmote;
}
if (emote)
{
this->emplace<EmoteElement>(emote.get(), flags);
return Success;
}
} // bttv/ffz emote
return Failure;
}
auto flags = MessageElementFlags(); auto flags = MessageElementFlags();
auto emote = boost::optional<EmotePtr>{}; auto emote = boost::optional<EmotePtr>{};
if ((emote = this->twitchChannel->globalBttv().emote(name))) // Emote order:
{ // - FrankerFaceZ Channel
flags = MessageElementFlag::BttvEmote; // - BetterTTV Channel
} // - FrankerFaceZ Global
else if ((emote = this->twitchChannel->bttvEmote(name))) // - BetterTTV Global
{ if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name)))
flags = MessageElementFlag::BttvEmote;
}
else if ((emote = this->twitchChannel->globalFfz().emote(name)))
{ {
flags = MessageElementFlag::FfzEmote; 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; flags = MessageElementFlag::FfzEmote;
} }
else if ((emote = globalBttvEmotes.emote(name)))
{
flags = MessageElementFlag::BttvEmote;
}
if (emote) if (emote)
{ {
@ -1064,11 +1023,11 @@ void TwitchMessageBuilder::appendTwitchBadges()
try try
{ {
if (twitchChannel) if (twitchChannel)
if (const auto &badge = this->twitchChannel->twitchBadge( if (const auto &_badge = this->twitchChannel->twitchBadge(
"bits", cheerAmount)) "bits", cheerAmount))
{ {
this->emplace<EmoteElement>( this->emplace<BadgeElement>(
badge.get(), MessageElementFlag::BadgeVanity) _badge.get(), MessageElementFlag::BadgeVanity)
->setTooltip(tooltip); ->setTooltip(tooltip);
continue; continue;
} }
@ -1079,10 +1038,10 @@ void TwitchMessageBuilder::appendTwitchBadges()
} }
// Use default bit badge // Use default bit badge
if (auto badge = this->twitchChannel->globalTwitchBadges().badge( if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
"bits", cheerAmount)) "bits", cheerAmount))
{ {
this->emplace<EmoteElement>(badge.get(), this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity) MessageElementFlag::BadgeVanity)
->setTooltip(tooltip); ->setTooltip(tooltip);
} }
@ -1112,7 +1071,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
{ {
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge()) if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
{ {
this->emplace<EmoteElement>( this->emplace<BadgeElement>(
customModBadge.get(), customModBadge.get(),
MessageElementFlag::BadgeChannelAuthority) MessageElementFlag::BadgeChannelAuthority)
->setTooltip((*customModBadge)->tooltip.string); ->setTooltip((*customModBadge)->tooltip.string);
@ -1172,7 +1131,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
if (auto badgeEmote = this->twitchChannel->twitchBadge( if (auto badgeEmote = this->twitchChannel->twitchBadge(
"subscriber", badge.mid(11))) "subscriber", badge.mid(11)))
{ {
this->emplace<EmoteElement>( this->emplace<BadgeElement>(
badgeEmote.get(), MessageElementFlag::BadgeSubscription) badgeEmote.get(), MessageElementFlag::BadgeSubscription)
->setTooltip((*badgeEmote)->tooltip.string); ->setTooltip((*badgeEmote)->tooltip.string);
continue; continue;
@ -1193,17 +1152,17 @@ void TwitchMessageBuilder::appendTwitchBadges()
if (auto badgeEmote = if (auto badgeEmote =
this->twitchChannel->twitchBadge(splits[0], splits[1])) this->twitchChannel->twitchBadge(splits[0], splits[1]))
{ {
this->emplace<EmoteElement>(badgeEmote.get(), this->emplace<BadgeElement>(badgeEmote.get(),
MessageElementFlag::BadgeVanity) MessageElementFlag::BadgeVanity)
->setTooltip((*badgeEmote)->tooltip.string); ->setTooltip((*badgeEmote)->tooltip.string);
continue; continue;
} }
if (auto badge = this->twitchChannel->globalTwitchBadges().badge( if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
splits[0], splits[1])) splits[0], splits[1]))
{ {
this->emplace<EmoteElement>(badge.get(), this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity) MessageElementFlag::BadgeVanity)
->setTooltip((*badge)->tooltip.string); ->setTooltip((*_badge)->tooltip.string);
continue; continue;
} }
} }
@ -1216,7 +1175,7 @@ void TwitchMessageBuilder::appendChatterinoBadges()
getApp()->chatterinoBadges->getBadge({this->userName}); getApp()->chatterinoBadges->getBadge({this->userName});
if (chatterinoBadgePtr) if (chatterinoBadgePtr)
{ {
this->emplace<EmoteElement>(*chatterinoBadgePtr, this->emplace<BadgeElement>(*chatterinoBadgePtr,
MessageElementFlag::BadgeChatterino); MessageElementFlag::BadgeChatterino);
} }
} }

View file

@ -41,7 +41,6 @@ public:
MessageParseArgs args; MessageParseArgs args;
const QVariantMap tags; const QVariantMap tags;
QString messageID;
QString userName; QString userName;
bool isIgnored() const; bool isIgnored() const;
@ -55,8 +54,10 @@ private:
void appendUsername(); void appendUsername();
void parseHighlights(bool isPastMsg); void parseHighlights(bool isPastMsg);
void appendTwitchEmote(const QString &emote, void appendTwitchEmote(
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec, std::vector<int> &correctPositions); const QString &emote,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
std::vector<int> &correctPositions);
Outcome tryAppendEmote(const EmoteName &name); Outcome tryAppendEmote(const EmoteName &name);
void addWords( void addWords(

View file

@ -156,6 +156,10 @@ void TwitchServer::messageReceived(Communi::IrcMessage *message)
{ {
handler.handleClearChatMessage(message); handler.handleClearChatMessage(message);
} }
else if (command == "CLEARMSG")
{
handler.handleClearMessageMessage(message);
}
else if (command == "USERSTATE") else if (command == "USERSTATE")
{ {
handler.handleUserStateMessage(message); handler.handleUserStateMessage(message);
@ -219,7 +223,7 @@ std::shared_ptr<Channel> TwitchServer::getCustomChannel(
{ {
static auto channel = static auto channel =
std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc); std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc);
static auto timer = [&] { static auto getTimer = [&] {
for (auto i = 0; i < 1000; i++) for (auto i = 0; i < 1000; i++)
{ {
channel->addMessage(makeSystemMessage(QString::number(i + 1))); channel->addMessage(makeSystemMessage(QString::number(i + 1)));
@ -264,7 +268,8 @@ std::shared_ptr<Channel> TwitchServer::getChannelOrEmptyByID(
if (!twitchChannel) if (!twitchChannel)
continue; continue;
if (twitchChannel->roomId() == channelId) if (twitchChannel->roomId() == channelId &&
twitchChannel->getName().splitRef(":").size() < 3)
{ {
return twitchChannel; return twitchChannel;
} }
@ -293,10 +298,11 @@ void TwitchServer::onMessageSendRequested(TwitchChannel *channel,
std::lock_guard<std::mutex> guard(this->lastMessageMutex_); std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
// std::queue<std::chrono::steady_clock::time_point> // std::queue<std::chrono::steady_clock::time_point>
auto &lastMessage = channel->hasModRights() ? this->lastMessageMod_ auto &lastMessage = channel->hasHighRateLimit()
: this->lastMessagePleb_; ? this->lastMessageMod_
size_t maxMessageCount = channel->hasModRights() ? 99 : 19; : this->lastMessagePleb_;
auto minMessageOffset = (channel->hasModRights() ? 100ms : 1100ms); size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
auto now = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now();

View file

@ -37,7 +37,7 @@ bool Paths::isPortable()
QString Paths::cacheDirectory() QString Paths::cacheDirectory()
{ {
static QStringSetting cachePathSetting = [] { static const auto pathSetting = [] {
QStringSetting cachePathSetting("/cache/path"); QStringSetting cachePathSetting("/cache/path");
cachePathSetting.connect([](const auto &newPath, auto) { cachePathSetting.connect([](const auto &newPath, auto) {
@ -47,9 +47,9 @@ QString Paths::cacheDirectory()
return cachePathSetting; return cachePathSetting;
}(); }();
auto path = cachePathSetting.getValue(); auto path = pathSetting.getValue();
if (path == "") if (path.isEmpty())
{ {
return this->cacheDirectory_; return this->cacheDirectory_;
} }

View file

@ -4,7 +4,7 @@
#include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/highlights/HighlightPhrase.hpp"
#include "controllers/moderationactions/ModerationAction.hpp" #include "controllers/moderationactions/ModerationAction.hpp"
#include "messages/HistoricMessageAppearance.hpp" #include "singletons/Toasts.hpp"
#include <pajlada/settings/setting.hpp> #include <pajlada/settings/setting.hpp>
#include <pajlada/settings/settinglistener.hpp> #include <pajlada/settings/settinglistener.hpp>
@ -32,15 +32,15 @@ public:
Qt::VerPattern}; Qt::VerPattern};
QStringSetting lastMessageColor = {"/appearance/messages/lastMessageColor", QStringSetting lastMessageColor = {"/appearance/messages/lastMessageColor",
""}; ""};
IntSetting historicMessagesAppearance = {
"/appearance/messages/historicMessagesAppearance",
HistoricMessageAppearance::Crossed | HistoricMessageAppearance::Greyed};
BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true}; BoolSetting showEmptyInput = {"/appearance/showEmptyInputBox", true};
BoolSetting showMessageLength = {"/appearance/messages/showMessageLength", BoolSetting showMessageLength = {"/appearance/messages/showMessageLength",
false}; false};
BoolSetting separateMessages = {"/appearance/messages/separateMessages", BoolSetting separateMessages = {"/appearance/messages/separateMessages",
false}; false};
BoolSetting compactEmotes = {"/appearance/messages/compactEmotes", true}; BoolSetting compactEmotes = {"/appearance/messages/compactEmotes", true};
BoolSetting hideModerated = {"/appearance/messages/hideModerated", false};
BoolSetting hideModerationActions = {
"/appearance/messages/hideModerationActions", false};
// BoolSetting collapseLongMessages = // BoolSetting collapseLongMessages =
// {"/appearance/messages/collapseLongMessages", false}; // {"/appearance/messages/collapseLongMessages", false};
@ -48,7 +48,7 @@ public:
"/appearance/messages/collapseMessagesMinLines", 0}; "/appearance/messages/collapseMessagesMinLines", 0};
BoolSetting alternateMessages = { BoolSetting alternateMessages = {
"/appearance/messages/alternateMessageBackground", false}; "/appearance/messages/alternateMessageBackground", false};
IntSetting boldScale = {"/appearance/boldScale", 57}; FloatSetting boldScale = {"/appearance/boldScale", 50};
BoolSetting showTabCloseButton = {"/appearance/showTabCloseButton", true}; BoolSetting showTabCloseButton = {"/appearance/showTabCloseButton", true};
BoolSetting showTabLive = {"/appearance/showTabLiveButton", false}; BoolSetting showTabLive = {"/appearance/showTabLiveButton", false};
BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton", BoolSetting hidePreferencesButton = {"/appearance/hidePreferencesButton",
@ -67,7 +67,6 @@ public:
BoolSetting headerUptime = {"/appearance/splitheader/showUptime", false}; BoolSetting headerUptime = {"/appearance/splitheader/showUptime", false};
FloatSetting customThemeMultiplier = {"/appearance/customThemeMultiplier", FloatSetting customThemeMultiplier = {"/appearance/customThemeMultiplier",
-0.5f}; -0.5f};
BoolSetting redDisabledMessages = {"/appearance/redStripes", true};
// BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame", // BoolSetting useCustomWindowFrame = {"/appearance/useCustomWindowFrame",
// false}; // false};
@ -106,10 +105,7 @@ public:
/// Emotes /// Emotes
BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight", BoolSetting scaleEmotesByLineHeight = {"/emotes/scaleEmotesByLineHeight",
false}; false};
BoolSetting enableTwitchEmotes = {"/emotes/enableTwitchEmotes", true}; BoolSetting enableEmoteImages = {"/emotes/enableEmoteImages", true};
BoolSetting enableBttvEmotes = {"/emotes/enableBTTVEmotes", true};
BoolSetting enableFfzEmotes = {"/emotes/enableFFZEmotes", true};
BoolSetting enableEmojis = {"/emotes/enableEmojis", true};
BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true}; BoolSetting animateEmotes = {"/emotes/enableGifAnimations", true};
FloatSetting emoteScale = {"/emotes/scale", 1.f}; FloatSetting emoteScale = {"/emotes/scale", 1.f};
@ -128,6 +124,7 @@ public:
/// Ingored Users /// Ingored Users
BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers",
true}; true};
IntSetting showIgnoredUsersMessages = {"/ignore/showIgnoredUsers", 0};
/// Moderation /// Moderation
QStringSetting timeoutAction = {"/moderation/timeoutAction", "Disable"}; QStringSetting timeoutAction = {"/moderation/timeoutAction", "Disable"};
@ -147,7 +144,7 @@ public:
"/highlighting/whisperHighlight/enableSound", false}; "/highlighting/whisperHighlight/enableSound", false};
BoolSetting enableWhisperHighlightTaskbar = { BoolSetting enableWhisperHighlightTaskbar = {
"/highlighting/whisperHighlight/enableTaskbarFlashing", false}; "/highlighting/whisperHighlight/enableTaskbarFlashing", false};
QStringSetting highlightColor = {"/highlighting/color", "#4B282C"}; QStringSetting highlightColor = {"/highlighting/color", ""};
BoolSetting longAlerts = {"/highlighting/alerts", false}; BoolSetting longAlerts = {"/highlighting/alerts", false};
@ -175,6 +172,8 @@ public:
"qrc:/sounds/ping3.wav"}; "qrc:/sounds/ping3.wav"};
BoolSetting notificationToast = {"/notifications/enableToast", false}; BoolSetting notificationToast = {"/notifications/enableToast", false};
IntSetting openFromToast = {"/notifications/openFromToast",
static_cast<int>(ToastReaction::OpenInBrowser)};
/// External tools /// External tools
// Streamlink // Streamlink
@ -188,6 +187,9 @@ public:
/// Misc /// Misc
IntSetting startUpNotification = {"/misc/startUpNotification", 0}; IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""}; QStringSetting currentVersion = {"/misc/currentVersion", ""};
BoolSetting loadTwitchMessageHistoryOnConnect = {
"/misc/twitch/loadMessageHistoryOnConnect", true};
IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 0};
QStringSetting cachePath = {"/cache/path", ""}; QStringSetting cachePath = {"/cache/path", ""};

View file

@ -27,9 +27,9 @@ void Theme::actuallyUpdate(double hue, double multiplier)
return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a);
}; };
auto sat = qreal(0); const auto sat = qreal(0);
auto isLight_ = this->isLightTheme(); const auto isLight = this->isLightTheme();
auto flat = isLight_; const auto flat = isLight;
if (this->isLightTheme()) if (this->isLightTheme())
{ {
@ -38,6 +38,9 @@ void Theme::actuallyUpdate(double hue, double multiplier)
this->splits.resizeHandle = QColor(0, 148, 255, 0xff); this->splits.resizeHandle = QColor(0, 148, 255, 0xff);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50); this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50);
// Highlighted Messages: theme support quick-fix
this->messages.backgrounds.highlighted = QColor("#BD8489");
} }
else else
{ {
@ -46,13 +49,16 @@ void Theme::actuallyUpdate(double hue, double multiplier)
this->splits.resizeHandle = QColor(0, 148, 255, 0x70); this->splits.resizeHandle = QColor(0, 148, 255, 0x70);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20); 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.background = getColor(0, sat, flat ? 1 : 0.9);
this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85); this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85);
this->splits.header.text = this->messages.textColors.regular; this->splits.header.text = this->messages.textColors.regular;
this->splits.header.focusedText = 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.background = getColor(0, sat, flat ? 0.95 : 0.95);
this->splits.input.border = getColor(0, sat, flat ? 1 : 1); 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() + "border:" + this->tabs.selected.backgrounds.regular.color().name() +
";" + "color:" + this->messages.textColors.regular.name() + ";" + // ";" + "color:" + this->messages.textColors.regular.name() + ";" + //
"selection-background-color:" + "selection-background-color:" +
(isLight_ ? "#68B1FF" (isLight ? "#68B1FF"
: this->tabs.selected.backgrounds.regular.color().name()); : this->tabs.selected.backgrounds.regular.color().name());
this->splits.input.focusedLine = this->tabs.highlighted.line.regular; this->splits.input.focusedLine = this->tabs.highlighted.line.regular;
this->splits.messageSeperator = 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.background = getColor(0, sat, 1);
this->splits.dropPreview = QColor(0, 148, 255, 0x30); this->splits.dropPreview = QColor(0, 148, 255, 0x30);
this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff); this->splits.dropPreviewBorder = QColor(0, 148, 255, 0xff);
// Highlighted Messages // Highlighted Messages
this->messages.backgrounds.highlighted = // hidden setting from PR #744 - if set it will overwrite theme color
QColor(getSettings()->highlightColor); // TODO: implement full theme support
if (getSettings()->highlightColor != "")
{
this->messages.backgrounds.highlighted =
QColor(getSettings()->highlightColor);
}
} }
void Theme::normalizeColor(QColor &color) void Theme::normalizeColor(QColor &color)

View file

@ -8,6 +8,8 @@
#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchServer.hpp"
#include "singletons/Paths.hpp" #include "singletons/Paths.hpp"
#include "util/StreamLink.hpp"
#include "widgets/helper/CommonTexts.hpp"
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
@ -25,13 +27,40 @@
namespace chatterino { namespace chatterino {
std::map<ToastReaction, QString> 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() bool Toasts::isEnabled()
{ {
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
return WinToastLib::WinToast::isCompatible() && return WinToastLib::WinToast::isCompatible() &&
getSettings()->notificationToast; getSettings()->notificationToast;
#endif #else
return false; 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<int> &value)
{
int i = static_cast<int>(value);
return Toasts::findStringFromReaction(static_cast<ToastReaction>(i));
} }
void Toasts::sendChannelNotification(const QString &channelName, Platform p) void Toasts::sendChannelNotification(const QString &channelName, Platform p)
@ -86,11 +115,33 @@ public:
void toastActivated() const void toastActivated() const
{ {
QString link; QString link;
if (platform_ == Platform::Twitch) auto toastReaction =
static_cast<ToastReaction>(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 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()); std::wstring widestr = std::wstring(utf8_text.begin(), utf8_text.end());
templ.setTextField(widestr, WinToastLib::WinToastTemplate::FirstLine); templ.setTextField(widestr, WinToastLib::WinToastTemplate::FirstLine);
templ.setTextField(L"Click here to open in browser", if (static_cast<ToastReaction>(getSettings()->openFromToast.getValue()) !=
WinToastLib::WinToastTemplate::SecondLine); ToastReaction::DontOpen)
{
QString mode =
Toasts::findStringFromReaction(getSettings()->openFromToast);
mode = mode.toLower();
templ.setTextField(L"Click here to " + mode.toStdWString(),
WinToastLib::WinToastTemplate::SecondLine);
}
QString Path; QString Path;
if (p == Platform::Twitch) if (p == Platform::Twitch)
{ {

View file

@ -7,10 +7,21 @@ namespace chatterino {
enum class Platform : uint8_t; enum class Platform : uint8_t;
enum class ToastReaction {
OpenInBrowser = 0,
OpenInPlayer = 1,
OpenInStreamlink = 2,
DontOpen = 3
};
class Toasts final : public Singleton class Toasts final : public Singleton
{ {
public: public:
void sendChannelNotification(const QString &channelName, Platform p); void sendChannelNotification(const QString &channelName, Platform p);
static QString findStringFromReaction(const ToastReaction &reaction);
static QString findStringFromReaction(
const pajlada::Settings::Setting<int> &reaction);
static std::map<ToastReaction, QString> reactionToString;
static bool isEnabled(); static bool isEnabled();
@ -18,6 +29,7 @@ private:
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
void sendWindowsNotification(const QString &channelName, Platform p); void sendWindowsNotification(const QString &channelName, Platform p);
#endif #endif
static void fetchChannelAvatar( static void fetchChannelAvatar(
const QString channelName, const QString channelName,
std::function<void(QString)> successCallback); std::function<void(QString)> successCallback);

View file

@ -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

View file

@ -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<pajlada::Signals::ScopedConnection> connections_;
};
} // namespace chatterino

View file

@ -22,6 +22,7 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QSaveFile>
#include <chrono> #include <chrono>
@ -75,10 +76,7 @@ WindowManager::WindowManager()
this->wordFlagsListener_.addSetting(settings->showBadgesSubscription); this->wordFlagsListener_.addSetting(settings->showBadgesSubscription);
this->wordFlagsListener_.addSetting(settings->showBadgesVanity); this->wordFlagsListener_.addSetting(settings->showBadgesVanity);
this->wordFlagsListener_.addSetting(settings->showBadgesChatterino); this->wordFlagsListener_.addSetting(settings->showBadgesChatterino);
this->wordFlagsListener_.addSetting(settings->enableBttvEmotes); this->wordFlagsListener_.addSetting(settings->enableEmoteImages);
this->wordFlagsListener_.addSetting(settings->enableEmojis);
this->wordFlagsListener_.addSetting(settings->enableFfzEmotes);
this->wordFlagsListener_.addSetting(settings->enableTwitchEmotes);
this->wordFlagsListener_.addSetting(settings->boldUsernames); this->wordFlagsListener_.addSetting(settings->boldUsernames);
this->wordFlagsListener_.addSetting(settings->lowercaseDomains); this->wordFlagsListener_.addSetting(settings->lowercaseDomains);
this->wordFlagsListener_.setCB([this] { this->wordFlagsListener_.setCB([this] {
@ -114,13 +112,12 @@ void WindowManager::updateWordTypeMask()
} }
// emotes // emotes
flags.set(settings->enableTwitchEmotes ? MEF::TwitchEmoteImage if (settings->enableEmoteImages)
: MEF::TwitchEmoteText); {
flags.set(settings->enableFfzEmotes ? MEF::FfzEmoteImage flags.set(MEF::EmoteImages);
: MEF::FfzEmoteText); }
flags.set(settings->enableBttvEmotes ? MEF::BttvEmoteImage flags.set(MEF::EmoteText);
: MEF::BttvEmoteText); flags.set(MEF::EmojiText);
flags.set(settings->enableEmojis ? MEF::EmojiImage : MEF::EmojiText);
// bits // bits
flags.set(MEF::BitsAmount); flags.set(MEF::BitsAmount);
@ -258,11 +255,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
// load file // load file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QFile file(settingsPath); QJsonArray windows_arr = this->loadWindowArray(settingsPath);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
QJsonArray windows_arr = document.object().value("windows").toArray();
// "deserialize" // "deserialize"
for (QJsonValue window_val : windows_arr) for (QJsonValue window_val : windows_arr)
@ -390,10 +383,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
void WindowManager::save() void WindowManager::save()
{ {
log("[WindowManager] Saving"); log("[WindowManager] Saving");
assertInGuiThread(); assertInGuiThread();
auto app = getApp();
QJsonDocument document; QJsonDocument document;
// "serialize" // "serialize"
@ -477,7 +467,7 @@ void WindowManager::save()
// save file // save file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME; QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QFile file(settingsPath); QSaveFile file(settingsPath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate); file.open(QIODevice::WriteOnly | QIODevice::Truncate);
QJsonDocument::JsonFormat format = QJsonDocument::JsonFormat format =
@ -489,7 +479,7 @@ void WindowManager::save()
; ;
file.write(document.toJson(format)); file.write(document.toJson(format));
file.flush(); file.commit();
} }
void WindowManager::sendAlert() void WindowManager::sendAlert()
@ -516,6 +506,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
case SplitNode::_Split: case SplitNode::_Split:
{ {
obj.insert("type", "split"); obj.insert("type", "split");
obj.insert("moderationMode", node->getSplit()->getModerationMode());
QJsonObject split; QJsonObject split;
encodeChannel(node->getSplit()->getIndirectChannel(), split); encodeChannel(node->getSplit()->getIndirectChannel(), split);
obj.insert("data", split); obj.insert("data", split);
@ -621,4 +612,14 @@ void WindowManager::incGeneration()
this->generation_++; 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 } // namespace chatterino

View file

@ -54,6 +54,7 @@ public:
virtual void initialize(Settings &settings, Paths &paths) override; virtual void initialize(Settings &settings, Paths &paths) override;
virtual void save() override; virtual void save() override;
void closeAll(); void closeAll();
QJsonArray loadWindowArray(const QString &settingsPath);
int getGeneration() const; int getGeneration() const;
void incGeneration(); void incGeneration();

View file

@ -33,20 +33,9 @@ LoggingChannel::LoggingChannel(const QString &_channelName)
// FOURTF: change this when adding more providers // FOURTF: change this when adding more providers
this->subDirectory = "Twitch/" + this->subDirectory; this->subDirectory = "Twitch/" + this->subDirectory;
auto app = getApp();
getSettings()->logPath.connect([this](const QString &logPath, auto) { getSettings()->logPath.connect([this](const QString &logPath, auto) {
auto app = getApp(); this->baseDirectory =
logPath.isEmpty() ? getPaths()->messageLogDirectory : logPath;
if (logPath.isEmpty())
{
this->baseDirectory = getPaths()->messageLogDirectory;
}
else
{
this->baseDirectory = logPath;
}
this->openLogFile(); this->openLogFile();
}); });
} }

View file

@ -36,8 +36,6 @@ namespace {
QString getStreamlinkProgram() QString getStreamlinkProgram()
{ {
auto app = getApp();
if (getSettings()->streamlinkUseCustomPath) if (getSettings()->streamlinkUseCustomPath)
{ {
return getSettings()->streamlinkPath + "/" + getBinaryName(); return getSettings()->streamlinkPath + "/" + getBinaryName();
@ -66,7 +64,6 @@ namespace {
{ {
static QErrorMessage *msg = new QErrorMessage; static QErrorMessage *msg = new QErrorMessage;
auto app = getApp();
if (getSettings()->streamlinkUseCustomPath) if (getSettings()->streamlinkUseCustomPath)
{ {
msg->showMessage( msg->showMessage(
@ -172,8 +169,6 @@ void getStreamQualities(const QString &channelURL,
void openStreamlink(const QString &channelURL, const QString &quality, void openStreamlink(const QString &channelURL, const QString &quality,
QStringList extraArguments) QStringList extraArguments)
{ {
auto app = getApp();
QStringList arguments; QStringList arguments;
QString additionalOptions = getSettings()->streamlinkOpts.getValue(); QString additionalOptions = getSettings()->streamlinkOpts.getValue();
@ -202,8 +197,6 @@ void openStreamlink(const QString &channelURL, const QString &quality,
void openStreamlinkForChannel(const QString &channel) void openStreamlinkForChannel(const QString &channel)
{ {
auto app = getApp();
QString channelURL = "twitch.tv/" + channel; QString channelURL = "twitch.tv/" + channel;
QString preferredQuality = getSettings()->preferredQuality; QString preferredQuality = getSettings()->preferredQuality;

View file

@ -14,6 +14,9 @@ AccountSwitchPopupWidget::AccountSwitchPopupWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
{ {
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
#ifdef Q_OS_LINUX
this->setWindowFlag(Qt::Popup);
#endif
this->setContentsMargins(0, 0, 0, 0); this->setContentsMargins(0, 0, 0, 0);

View file

@ -6,11 +6,11 @@
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/InitUpdateButton.hpp" #include "util/InitUpdateButton.hpp"
#include "util/Shortcut.hpp"
#include "widgets/Window.hpp" #include "widgets/Window.hpp"
#include "widgets/dialogs/SettingsDialog.hpp" #include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/helper/NotebookButton.hpp" #include "widgets/helper/NotebookButton.hpp"
#include "widgets/helper/NotebookTab.hpp" #include "widgets/helper/NotebookTab.hpp"
#include "util/Shortcut.hpp"
#include "widgets/splits/Split.hpp" #include "widgets/splits/Split.hpp"
#include "widgets/splits/SplitContainer.hpp" #include "widgets/splits/SplitContainer.hpp"

View file

@ -110,7 +110,7 @@ void Scrollbar::setSmallChange(qreal value)
void Scrollbar::setDesiredValue(qreal value, bool animated) void Scrollbar::setDesiredValue(qreal value, bool animated)
{ {
animated &= getSettings()->enableSmoothScrolling.getValue(); animated &= getSettings()->enableSmoothScrolling;
value = std::max(this->minimum_, value = std::max(this->minimum_,
std::min(this->maximum_ - this->largeChange_, value)); std::min(this->maximum_ - this->largeChange_, value));

View file

@ -29,6 +29,7 @@
#include <QShortcut> #include <QShortcut>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QMenuBar>
#include <QStandardItemModel> #include <QStandardItemModel>
namespace chatterino { namespace chatterino {
@ -43,6 +44,10 @@ Window::Window(WindowType type)
this->addShortcuts(); this->addShortcuts();
this->addLayout(); this->addLayout();
#ifdef Q_OS_MACOS
this->addMenuBar();
#endif
this->signalHolder_.managedConnect( this->signalHolder_.managedConnect(
getApp()->accounts->twitch.currentUserChanged, getApp()->accounts->twitch.currentUserChanged,
[this] { this->onAccountSelected(); }); [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 UGLYMACROHACK1(s) #s
#define UGLYMACROHACK(s) UGLYMACROHACK1(s) #define UGLYMACROHACK(s) UGLYMACROHACK1(s)

View file

@ -38,6 +38,7 @@ private:
void addShortcuts(); void addShortcuts();
void addLayout(); void addLayout();
void onAccountSelected(); void onAccountSelected();
void addMenuBar();
WindowType type_; WindowType type_;

View file

@ -319,7 +319,6 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
{ {
return false; return false;
} }
return true;
} }
else if (event->type() == QEvent::KeyRelease) else if (event->type() == QEvent::KeyRelease)
{ {

View file

@ -84,6 +84,8 @@ UserInfoPopup::UserInfoPopup()
.assign(&this->ui_.ignoreHighlights); .assign(&this->ui_.ignoreHighlights);
auto viewLogs = user.emplace<EffectLabel2>(this); auto viewLogs = user.emplace<EffectLabel2>(this);
viewLogs->getLabel().setText("Online logs"); viewLogs->getLabel().setText("Online logs");
auto usercard = user.emplace<EffectLabel2>(this);
usercard->getLabel().setText("Usercard");
auto mod = user.emplace<Button>(this); auto mod = user.emplace<Button>(this);
mod->setPixmap(app->resources->buttons.mod); mod->setPixmap(app->resources->buttons.mod);
@ -103,6 +105,12 @@ UserInfoPopup::UserInfoPopup()
logs->show(); logs->show();
}); });
QObject::connect(usercard.getElement(), &Button::leftClicked, [this] {
QDesktopServices::openUrl("https://www.twitch.tv/popout/" +
this->channel_->getName() +
"/viewercard/" + this->userName_);
});
QObject::connect(mod.getElement(), &Button::leftClicked, [this] { QObject::connect(mod.getElement(), &Button::leftClicked, [this] {
this->channel_->sendMessage("/mod " + this->userName_); this->channel_->sendMessage("/mod " + this->userName_);
}); });
@ -128,8 +136,8 @@ UserInfoPopup::UserInfoPopup()
this->userName_, Qt::CaseInsensitive) == 0; this->userName_, Qt::CaseInsensitive) == 0;
visibilityMod = twitchChannel->isBroadcaster() && !isMyself; visibilityMod = twitchChannel->isBroadcaster() && !isMyself;
visibilityUnmod = visibilityMod || visibilityUnmod =
(twitchChannel->isMod() && isMyself); visibilityMod || (twitchChannel->isMod() && isMyself);
} }
mod->setVisible(visibilityMod); mod->setVisible(visibilityMod);
unmod->setVisible(visibilityUnmod); unmod->setVisible(visibilityUnmod);
@ -147,8 +155,8 @@ UserInfoPopup::UserInfoPopup()
TwitchChannel *twitchChannel = TwitchChannel *twitchChannel =
dynamic_cast<TwitchChannel *>(this->channel_.get()); dynamic_cast<TwitchChannel *>(this->channel_.get());
bool hasModRights = twitchChannel ? twitchChannel->hasModRights() bool hasModRights =
: false; twitchChannel ? twitchChannel->hasModRights() : false;
lineMod->setVisible(hasModRights); lineMod->setVisible(hasModRights);
timeout->setVisible(hasModRights); timeout->setVisible(hasModRights);
}); });

View file

@ -15,6 +15,7 @@
#include "providers/twitch/TwitchServer.hpp" #include "providers/twitch/TwitchServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/DistanceBetweenPoints.hpp" #include "util/DistanceBetweenPoints.hpp"
#include "util/IncognitoBrowser.hpp" #include "util/IncognitoBrowser.hpp"
@ -294,8 +295,12 @@ void ChannelView::scaleChangedEvent(float scale)
if (this->goToBottom_) if (this->goToBottom_)
{ {
auto factor = this->qtFontScale();
#ifdef Q_OS_MACOS
factor = scale * 80.f / this->logicalDpiX() * this->devicePixelRatioF();
#endif
this->goToBottom_->getLabel().setFont( this->goToBottom_->getLabel().setFont(
getFonts()->getFont(FontStyle::UiMedium, this->qtFontScale())); getFonts()->getFont(FontStyle::UiMedium, factor));
} }
} }
@ -307,6 +312,7 @@ void ChannelView::queueUpdate()
// } // }
// this->repaint(); // this->repaint();
this->update(); this->update();
// this->updateTimer.start(); // this->updateTimer.start();
@ -1213,6 +1219,28 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
} }
else else
{ {
auto &tooltipPreviewImage = TooltipPreviewImage::getInstance();
auto emoteElement = dynamic_cast<const EmoteElement *>(
&hoverLayoutElement->getCreator());
if (emoteElement && getSettings()->emotesTooltipPreview.getValue())
{
if (event->modifiers() == Qt::ShiftModifier ||
getSettings()->emotesTooltipPreview.getValue() == 1)
{
tooltipPreviewImage.setImage(
emoteElement->getEmote()->images.getImage(3.0));
}
else
{
tooltipPreviewImage.setImage(nullptr);
}
}
else
{
tooltipPreviewImage.setImage(nullptr);
}
tooltipWidget->moveTo(this, event->globalPos()); tooltipWidget->moveTo(this, event->globalPos());
tooltipWidget->setWordWrap(isLinkValid); tooltipWidget->setWordWrap(isLinkValid);
tooltipWidget->setText(tooltip); tooltipWidget->setText(tooltip);
@ -1667,7 +1695,12 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
case Link::UserAction: case Link::UserAction:
{ {
QString value = link.value; QString value = link.value;
value.replace("{user}", layout->getMessage()->loginName);
value.replace("{user}", layout->getMessage()->loginName)
.replace("{channel}", this->channel_->getName())
.replace("{msg-id}", layout->getMessage()->id)
.replace("{message}", layout->getMessage()->messageText);
this->channel_->sendMessage(value); this->channel_->sendMessage(value);
} }
break; break;

View file

@ -4,6 +4,7 @@
#include "messages/LimitedQueue.hpp" #include "messages/LimitedQueue.hpp"
#include "messages/LimitedQueueSnapshot.hpp" #include "messages/LimitedQueueSnapshot.hpp"
#include "messages/Selection.hpp" #include "messages/Selection.hpp"
#include "messages/Image.hpp"
#include "widgets/BaseWidget.hpp" #include "widgets/BaseWidget.hpp"
#include <QPaintEvent> #include <QPaintEvent>
@ -25,7 +26,7 @@ using ChannelPtr = std::shared_ptr<Channel>;
struct Message; struct Message;
using MessagePtr = std::shared_ptr<const Message>; using MessagePtr = std::shared_ptr<const Message>;
enum class MessageFlag : uint16_t; enum class MessageFlag : uint32_t;
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;
class MessageLayout; class MessageLayout;

View file

@ -0,0 +1,6 @@
#pragma once
#define OPEN_IN_BROWSER "Open in browser"
#define OPEN_PLAYER_IN_BROWSER "Open in player in browser"
#define OPEN_IN_STREAMLINK "Open in streamlink"
#define DONT_OPEN "Don't open"

View file

@ -29,8 +29,6 @@ NotebookTab::NotebookTab(Notebook *notebook)
, notebook_(notebook) , notebook_(notebook)
, menu_(this) , menu_(this)
{ {
auto app = getApp();
this->setAcceptDrops(true); this->setAcceptDrops(true);
this->positionChangedAnimation_.setEasingCurve( this->positionChangedAnimation_.setEasingCurve(
@ -527,8 +525,6 @@ void NotebookTab::dragEnterEvent(QDragEnterEvent *event)
void NotebookTab::mouseMoveEvent(QMouseEvent *event) void NotebookTab::mouseMoveEvent(QMouseEvent *event)
{ {
auto app = getApp();
if (getSettings()->showTabCloseButton && if (getSettings()->showTabCloseButton &&
this->notebook_->getAllowUserTabManagement()) // this->notebook_->getAllowUserTabManagement()) //
{ {

View file

@ -98,7 +98,7 @@ void ResizingTextEdit::keyPressEvent(QKeyEvent *event)
QString currentCompletionPrefix = this->textUnderCursor(); QString currentCompletionPrefix = this->textUnderCursor();
// check if there is something to complete // check if there is something to complete
if (!currentCompletionPrefix.size()) if (currentCompletionPrefix.size() <= 1)
{ {
return; return;
} }
@ -228,6 +228,27 @@ void ResizingTextEdit::insertCompletion(const QString &completion)
this->setTextCursor(tc); this->setTextCursor(tc);
} }
bool ResizingTextEdit::canInsertFromMimeData(const QMimeData *source) const
{
if (source->hasImage())
{
return false;
}
else if (source->hasFormat("text/plain"))
{
return true;
}
return false;
}
void ResizingTextEdit::insertFromMimeData(const QMimeData *source)
{
if (!source->hasImage())
{
insertPlainText(source->text());
}
}
QCompleter *ResizingTextEdit::getCompleter() const QCompleter *ResizingTextEdit::getCompleter() const
{ {
return this->completer_; return this->completer_;

View file

@ -30,6 +30,9 @@ protected:
void focusInEvent(QFocusEvent *event) override; void focusInEvent(QFocusEvent *event) override;
void focusOutEvent(QFocusEvent *event) override; void focusOutEvent(QFocusEvent *event) override;
bool canInsertFromMimeData(const QMimeData *source) const override;
void insertFromMimeData(const QMimeData *source) override;
private: private:
// hadSpace is set to true in case the "textUnderCursor" word was after a // hadSpace is set to true in case the "textUnderCursor" word was after a
// space // space

View file

@ -17,6 +17,25 @@ SearchPopup::SearchPopup()
this->resize(400, 600); this->resize(400, 600);
} }
void SearchPopup::setChannel(ChannelPtr channel)
{
this->snapshot_ = channel->getMessageSnapshot();
this->performSearch();
this->setWindowTitle("Searching in " + channel->getName() + "s history");
}
void SearchPopup::keyPressEvent(QKeyEvent *e)
{
if (e->key() == Qt::Key_Escape)
{
this->close();
return;
}
BaseWidget::keyPressEvent(e);
}
void SearchPopup::initLayout() void SearchPopup::initLayout()
{ {
// VBOX // VBOX
@ -60,14 +79,6 @@ void SearchPopup::initLayout()
} }
} }
void SearchPopup::setChannel(ChannelPtr channel)
{
this->snapshot_ = channel->getMessageSnapshot();
this->performSearch();
this->setWindowTitle("Searching in " + channel->getName() + "s history");
}
void SearchPopup::performSearch() void SearchPopup::performSearch()
{ {
QString text = searchInput_->text(); QString text = searchInput_->text();

View file

@ -22,6 +22,9 @@ public:
void setChannel(std::shared_ptr<Channel> channel); void setChannel(std::shared_ptr<Channel> channel);
protected:
void keyPressEvent(QKeyEvent *e) override;
private: private:
void initLayout(); void initLayout();
void performSearch(); void performSearch();

View file

@ -113,6 +113,7 @@ AboutPage::AboutPage()
l.emplace<QLabel>("Messenger emojis provided by <a href=\"https://facebook.com\">Facebook</a>")->setOpenExternalLinks(true); l.emplace<QLabel>("Messenger emojis provided by <a href=\"https://facebook.com\">Facebook</a>")->setOpenExternalLinks(true);
l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a>" l.emplace<QLabel>("Emoji datasource provided by <a href=\"https://www.iamcal.com/\">Cal Henderson</a>"
"(<a href=\"https://github.com/iamcal/emoji-data/blob/master/LICENSE\">show license</a>)")->setOpenExternalLinks(true); "(<a href=\"https://github.com/iamcal/emoji-data/blob/master/LICENSE\">show license</a>)")->setOpenExternalLinks(true);
l.emplace<QLabel>("Twitch emote data provided by <a href=\"https://twitchemotes.com/\">twitchemotes.com</a> through the <a href=\"https://github.com/Chatterino/api\">Chatterino API</a>")->setOpenExternalLinks(true);
// clang-format on // clang-format on
} }

View file

@ -26,7 +26,6 @@ namespace chatterino {
AdvancedPage::AdvancedPage() AdvancedPage::AdvancedPage()
: SettingsPage("Advanced", ":/settings/advanced.svg") : SettingsPage("Advanced", ":/settings/advanced.svg")
{ {
auto app = getApp();
LayoutCreator<AdvancedPage> layoutCreator(this); LayoutCreator<AdvancedPage> layoutCreator(this);
auto tabs = layoutCreator.emplace<QTabWidget>(); auto tabs = layoutCreator.emplace<QTabWidget>();

View file

@ -14,8 +14,6 @@ namespace chatterino {
ExternalToolsPage::ExternalToolsPage() ExternalToolsPage::ExternalToolsPage()
: SettingsPage("External tools", ":/settings/externaltools.svg") : SettingsPage("External tools", ":/settings/externaltools.svg")
{ {
auto app = getApp();
LayoutCreator<ExternalToolsPage> layoutCreator(this); LayoutCreator<ExternalToolsPage> layoutCreator(this);
auto layout = layoutCreator.setLayoutType<QVBoxLayout>(); auto layout = layoutCreator.setLayoutType<QVBoxLayout>();

View file

@ -189,6 +189,9 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Show tab close button", s.showTabCloseButton); layout.addCheckbox("Show tab close button", s.showTabCloseButton);
layout.addCheckbox("Show input when empty", s.showEmptyInput); layout.addCheckbox("Show input when empty", s.showEmptyInput);
layout.addCheckbox("Show input message length", s.showMessageLength); layout.addCheckbox("Show input message length", s.showMessageLength);
layout.addCheckbox("Hide preferences button (ctrl+p to show)",
s.hidePreferencesButton);
layout.addCheckbox("Hide user button", s.hideUserButton);
layout.addTitle("Messages"); layout.addTitle("Messages");
layout.addCheckbox("Timestamps", s.showTimestamps); layout.addCheckbox("Timestamps", s.showTimestamps);
@ -197,8 +200,8 @@ void GeneralPage::initLayout(SettingsLayout &layout)
s.timestampFormat, true); s.timestampFormat, true);
layout.addDropdown<int>( layout.addDropdown<int>(
"Collapse messages", "Collapse messages",
{"Never", "Longer than 2 lines", "Longer than 3 lines", {"Never", "After 2 lines", "After 3 lines", "After 4 lines",
"Longer than 4 lines", "Longer than 5 lines"}, "After 5 lines"},
s.collpseMessagesMinLines, s.collpseMessagesMinLines,
[](auto val) { [](auto val) {
return val ? QString("After ") + QString::number(val) + " lines" return val ? QString("After ") + QString::number(val) + " lines"
@ -209,6 +212,8 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Alternate background color", s.alternateMessages); layout.addCheckbox("Alternate background color", s.alternateMessages);
// layout.addCheckbox("Mark last message you read"); // layout.addCheckbox("Mark last message you read");
// layout.addDropdown("Last read message style", {"Default"}); // layout.addDropdown("Last read message style", {"Default"});
layout.addCheckbox("Hide moderated messages", s.hideModerated);
layout.addCheckbox("Hide moderation messages", s.hideModerationActions);
layout.addTitle("Emotes"); layout.addTitle("Emotes");
layout.addDropdown<float>( layout.addDropdown<float>(
@ -223,6 +228,7 @@ void GeneralPage::initLayout(SettingsLayout &layout)
[](auto args) { return fuzzyToFloat(args.value, 1.f); }); [](auto args) { return fuzzyToFloat(args.value, 1.f); });
layout.addCheckbox("Gif animations", s.animateEmotes); layout.addCheckbox("Gif animations", s.animateEmotes);
layout.addCheckbox("Animate only when focused", s.animationsWhenFocused); layout.addCheckbox("Animate only when focused", s.animationsWhenFocused);
layout.addCheckbox("Emote images", s.enableEmoteImages);
layout.addDropdown("Emoji set", layout.addDropdown("Emoji set",
{"EmojiOne 2", "EmojiOne 3", "Twitter", "Facebook", {"EmojiOne 2", "EmojiOne 3", "Twitter", "Facebook",
"Apple", "Google", "Messenger"}, "Apple", "Google", "Messenger"},
@ -249,63 +255,35 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addTitle("Miscellaneous"); layout.addTitle("Miscellaneous");
layout.addCheckbox("Show joined users (< 1000 chatters)", s.showJoins); layout.addCheckbox("Show joined users (< 1000 chatters)", s.showJoins);
layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts); layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts);
layout.addDropdown("Boldness", {"Not implemented"});
layout.addCheckbox("Lowercase domains", s.lowercaseDomains); layout.addCheckbox("Lowercase domains", s.lowercaseDomains);
layout.addCheckbox("Bold @usernames", s.boldUsernames); layout.addCheckbox("Bold @usernames", s.boldUsernames);
layout.addDropdown<float>(
"Username font weight", {"0", "25", "Default", "75", "100"},
s.boldScale,
[](auto val) {
if (val == 50)
return QString("Default");
else
return QString::number(val);
},
[](auto args) { return fuzzyToFloat(args.value, 50.f); });
layout.addCheckbox("Show link info when hovering", s.linkInfoTooltip); layout.addCheckbox("Show link info when hovering", s.linkInfoTooltip);
layout.addCheckbox("Double click links to open", s.linksDoubleClickOnly); layout.addCheckbox("Double click links to open", s.linksDoubleClickOnly);
layout.addCheckbox("Unshorten links", s.unshortLinks); layout.addCheckbox("Unshorten links", s.unshortLinks);
layout.addCheckbox("Show live indicator in tabs", s.showTabLive); layout.addCheckbox("Show live indicator in tabs", s.showTabLive);
layout.addDropdown<int>(
"Show emote preview in tooltip on hover",
{"Don't show", "Always show", "Hold shift"}, s.emotesTooltipPreview,
[](int index) { return index; }, [](auto args) { return args.index; },
false);
layout.addSpacing(16); layout.addSpacing(16);
layout.addSeperator(); layout.addSeperator();
layout.addTitle2("Misc"); layout.addTitle2("Miscellaneous (Twitch)");
layout.addCheckbox("Show twitch whispers inline", s.inlineWhispers); layout.addCheckbox("Show twitch whispers inline", s.inlineWhispers);
layout.addDropdown<int>( layout.addCheckbox("Load message history on connect",
"Historic messages appearance", s.loadTwitchMessageHistoryOnConnect);
{"Crossed and Greyed", "Crossed", "Greyed", "No change"},
s.historicMessagesAppearance,
[](auto val) {
if (val & HistoricMessageAppearance::Crossed &&
val & HistoricMessageAppearance::Greyed)
{
return QString("Crossed and Greyed");
}
else if (val & HistoricMessageAppearance::Crossed)
{
return QString("Crossed");
}
else if (val & HistoricMessageAppearance::Greyed)
{
return QString("Greyed");
}
else
{
return QString("No Change");
}
},
[](auto args) -> int {
switch (args.index)
{
default:
case 0:
return HistoricMessageAppearance::Crossed |
HistoricMessageAppearance::Greyed;
break;
case 1:
return HistoricMessageAppearance::Crossed;
break;
case 2:
return HistoricMessageAppearance::Greyed;
break;
case 3:
return 0;
break;
}
},
false);
layout.addCheckbox("Emphasize deleted messages", s.redDisabledMessages);
/* /*
layout.addTitle2("Cache"); layout.addTitle2("Cache");

View file

@ -77,8 +77,24 @@ void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> users,
auto anyways = users.emplace<QHBoxLayout>().withoutMargin(); auto anyways = users.emplace<QHBoxLayout>().withoutMargin();
{ {
anyways.emplace<QLabel>("Show anyways if:"); anyways.emplace<QLabel>("Show messages from ignored users anyways:");
anyways.emplace<QComboBox>();
auto combo = anyways.emplace<QComboBox>().getElement();
combo->addItems(
{"Never", "If you are Moderator", "If you are Broadcaster"});
auto &setting = getSettings()->showIgnoredUsersMessages;
setting.connect(
[combo](const int value) { combo->setCurrentIndex(value); });
QObject::connect(combo,
QOverload<int>::of(&QComboBox::currentIndexChanged),
[&setting](int index) {
if (index != -1)
setting = index;
});
anyways->addStretch(1); anyways->addStretch(1);
} }

View file

@ -43,7 +43,8 @@ KeyboardSettingsPage::KeyboardSettingsPage()
new QLabel("Search in current channel")); new QLabel("Search in current channel"));
form->addRow(new QLabel("Ctrl + E"), new QLabel("Open Emote menu")); form->addRow(new QLabel("Ctrl + E"), new QLabel("Open Emote menu"));
form->addRow(new QLabel("Ctrl + P"), new QLabel("Open Settings menu")); form->addRow(new QLabel("Ctrl + P"), new QLabel("Open Settings menu"));
form->addRow(new QLabel("F5"), new QLabel("Reload subscriber and channel emotes")); form->addRow(new QLabel("F5"),
new QLabel("Reload subscriber and channel emotes"));
} }
} // namespace chatterino } // namespace chatterino

View file

@ -182,12 +182,6 @@ void LookPage::addMessageTab(LayoutCreator<QVBoxLayout> layout)
layout.append( layout.append(
this->createCheckBox("Compact emotes", getSettings()->compactEmotes)); this->createCheckBox("Compact emotes", getSettings()->compactEmotes));
/// greyOutHistoricMessages setting changed by hemirt from checkbox to
/// historicMessagesBehaviour dropdown QString option
// layout.append(this->createCheckBox("Grey out historic messages",
// getSettings()->greyOutHistoricMessages));
///
// --
layout.emplace<Line>(false); layout.emplace<Line>(false);
// bold-slider // bold-slider

View file

@ -61,15 +61,10 @@ QString formatSize(qint64 size)
QString fetchLogDirectorySize() QString fetchLogDirectorySize()
{ {
QString logPathDirectory; QString logPathDirectory = getSettings()->logPath.getValue().isEmpty()
if (getSettings()->logPath == "") ? getPaths()->messageLogDirectory
{ : getSettings()->logPath;
logPathDirectory = getPaths()->messageLogDirectory;
}
else
{
logPathDirectory = getSettings()->logPath;
}
qint64 logsSize = dirSize(logPathDirectory); qint64 logsSize = dirSize(logPathDirectory);
QString logsSizeLabel = "Your logs currently take up "; QString logsSizeLabel = "Your logs currently take up ";
logsSizeLabel += formatSize(logsSize); logsSizeLabel += formatSize(logsSize);
@ -96,31 +91,22 @@ ModerationPage::ModerationPage()
QtConcurrent::run([] { return fetchLogDirectorySize(); })); QtConcurrent::run([] { return fetchLogDirectorySize(); }));
// Logs (copied from LoggingMananger) // Logs (copied from LoggingMananger)
getSettings()->logPath.connect( getSettings()->logPath.connect([logsPathLabel](const QString &logPath,
[logsPathLabel](const QString &logPath, auto) mutable { auto) mutable {
QString pathOriginal; QString pathOriginal =
logPath.isEmpty() ? getPaths()->messageLogDirectory : logPath;
if (logPath == "") QString pathShortened =
{ "Logs are saved at <a href=\"file:///" + pathOriginal +
pathOriginal = getPaths()->messageLogDirectory; "\"><span style=\"color: white;\">" +
} shortenString(pathOriginal, 50) + "</span></a>";
else
{
pathOriginal = logPath;
}
QString pathShortened = logsPathLabel->setText(pathShortened);
"Logs are saved at <a href=\"file:///" + pathOriginal + logsPathLabel->setToolTip(pathOriginal);
"\"><span style=\"color: white;\">" + });
shortenString(pathOriginal, 50) + "</span></a>";
logsPathLabel->setText(pathShortened);
logsPathLabel->setToolTip(pathOriginal);
});
logsPathLabel->setTextFormat(Qt::RichText); logsPathLabel->setTextFormat(Qt::RichText);
logsPathLabel->setTextInteractionFlags(Qt::TextBrowserInteraction | logsPathLabel->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard |
Qt::LinksAccessibleByKeyboard); Qt::LinksAccessibleByKeyboard);
logsPathLabel->setOpenExternalLinks(true); logsPathLabel->setOpenExternalLinks(true);
logs.append(this->createCheckBox("Enable logging", logs.append(this->createCheckBox("Enable logging",
@ -161,7 +147,8 @@ ModerationPage::ModerationPage()
// clang-format off // clang-format off
auto label = modMode.emplace<QLabel>( auto label = modMode.emplace<QLabel>(
"Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>" "Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>"
"Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\" or any other custom text commands.<br>"); "Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\", \"/w someusername !report {user} was bad in channel {channel}\" or any other custom text commands.<br>"
"For deleting messages use /delete {msg-id}.");
label->setWordWrap(true); label->setWordWrap(true);
label->setStyleSheet("color: #bbb"); label->setStyleSheet("color: #bbb");
// clang-format on // clang-format on

View file

@ -4,6 +4,7 @@
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "controllers/notifications/NotificationModel.hpp" #include "controllers/notifications/NotificationModel.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/Toasts.hpp"
#include "util/LayoutCreator.hpp" #include "util/LayoutCreator.hpp"
#include "widgets/helper/EditableModelView.hpp" #include "widgets/helper/EditableModelView.hpp"
@ -23,7 +24,6 @@ NotificationPage::NotificationPage()
: SettingsPage("Notifications", ":/settings/notification2.svg") : SettingsPage("Notifications", ":/settings/notification2.svg")
{ {
LayoutCreator<NotificationPage> layoutCreator(this); LayoutCreator<NotificationPage> layoutCreator(this);
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin(); auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
{ {
auto tabs = layout.emplace<QTabWidget>(); auto tabs = layout.emplace<QTabWidget>();
@ -39,6 +39,23 @@ NotificationPage::NotificationPage()
settings.append( settings.append(
this->createCheckBox("Enable toasts (Windows 8 or later)", this->createCheckBox("Enable toasts (Windows 8 or later)",
getSettings()->notificationToast)); getSettings()->notificationToast));
auto openIn = settings.emplace<QHBoxLayout>().withoutMargin();
{
openIn.emplace<QLabel>("Open stream from Toast: ")
->setSizePolicy(QSizePolicy::Maximum,
QSizePolicy::Preferred);
// implementation of custom combobox done
// because addComboBox only can handle strings-settings
// int setting for the ToastReaction is desired
openIn
.append(this->createToastReactionComboBox(
this->managedConnections_))
->setSizePolicy(QSizePolicy::Maximum,
QSizePolicy::Preferred);
}
openIn->setContentsMargins(40, 0, 0, 0);
openIn->setSizeConstraint(QLayout::SetMaximumSize);
#endif #endif
auto customSound = auto customSound =
layout.emplace<QHBoxLayout>().withoutMargin(); layout.emplace<QHBoxLayout>().withoutMargin();
@ -117,4 +134,31 @@ NotificationPage::NotificationPage()
} }
} }
} }
QComboBox *NotificationPage::createToastReactionComboBox(
std::vector<pajlada::Signals::ScopedConnection> managedConnections)
{
QComboBox *toastReactionOptions = new QComboBox();
for (int i = 0; i <= static_cast<int>(ToastReaction::DontOpen); i++)
{
toastReactionOptions->insertItem(
i, Toasts::findStringFromReaction(static_cast<ToastReaction>(i)));
}
// update when setting changes
pajlada::Settings::Setting<int> setting = getSettings()->openFromToast;
setting.connect(
[toastReactionOptions](const int &index, auto) {
toastReactionOptions->setCurrentIndex(index);
},
managedConnections);
QObject::connect(toastReactionOptions,
QOverload<int>::of(&QComboBox::currentIndexChanged),
[](const int &newValue) {
getSettings()->openFromToast.setValue(newValue);
});
return toastReactionOptions;
}
} // namespace chatterino } // namespace chatterino

View file

@ -15,6 +15,8 @@ public:
NotificationPage(); NotificationPage();
private: private:
QComboBox *createToastReactionComboBox(
std::vector<pajlada::Signals::ScopedConnection> managedConnections);
}; };
} // namespace chatterino } // namespace chatterino

Some files were not shown because too many files have changed in this diff Show more