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"]
path = lib/appbase
url = https://github.com/fourtf/appbase
url = https://github.com/Chatterino/appbase

View file

@ -1,10 +1,35 @@
before_install:
- sudo add-apt-repository --yes ppa:ubuntu-sdk-team/ppa
- sudo apt-get update -qq
- sudo apt-get install qtbase5-dev qtdeclarative5-dev libqt5webkit5-dev libsqlite3-dev
- sudo apt-get install qt5-default qttools5-dev-tools
os: osx
osx_image: xcode10.2
addons:
homebrew:
packages:
- boost
- openssl
- rapidjson
- qt
- p7zip
compiler: clang
script:
- qmake -qt=qt5 -v
- qmake -qt=qt5
- make
- mkdir build && cd build
- /usr/local/opt/qt/bin/qmake .. && make -j8
- /usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg
- mv chatterino.dmg chatterino-osx.dmg
before_deploy:
- git config --global user.email "builds@travis-ci.com"
- git config --global user.name "Travis CI"
- export GIT_TAG=nightly-build
- git tag $GIT_TAG -f
deploy:
skip_cleanup: true
provider: releases
api_key:
secure: ZzS55wlwtLAVEBaDDMqiuqZwuTpvLbNnaNw0enfiqpjWT7hgbbp/SBw2rbYIkVqm7tBHCLnEzKto6p4Gz6ROo0gGACARmx7EwIloX18rMCuBWygNHRyVruDSlmEOLWRqYByDbUdCkKhYr9aegnkm7zhzCmSBCTW28/uVlxM2bTHIgqKEpB4k1W8OqKdJDxqZKeF4r7nDNSOx5ylhpiK+WNFK8yfiaF1SQlSwsdv9o1RkbJlew7iigvHvEM2kDMkiMWYlJ2khkUWVCVQDQGe4/ya5pgTIHDLu5sZuclp5zhgfDf1U3STvsbQWvxJfsmCId7IQHJ83OSFeoUf6y849i3GMqlNi3aXrxEx0fi0dILQ76/Sj246FPMA4kC0/W49uaxqD784wFuJDjSWeWwi/NPoJ/gz0mGZy+08BoztOGqqOKjJJdESBYTio71N8VcK09zQ0LjXRmX+g3BbrK6a2F3hiMKeuYwdaN2/KdMMoqFDau6L3fXLdpcHKdJC8K/yzJtyyIe0CRB2nj8sZLHfxDwoRm7gOTDXq1zPL7CP9cCwCnCR6nm3CqUW/CnSWuMKpSoQRlP5EBI7zzYT2/tZc/vat5nob7Xif6yFF9fh/VHx4tC6zsfkA1nPPN3+QpdVInRo7dCVxtTqey5FdVjSiv7n11TrFhZ7+Fr5x6CZqa58=
file: "chatterino-osx.dmg"
prerelease: true
on:
branch: nightly

View file

@ -4,7 +4,7 @@ Note on Qt version compatibility: If you are installing Qt from a package manage
## Ubuntu 18.04
*most likely works the same for other Debian-like distros*
1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev`
1. Install dependencies (and the C++ IDE Qt Creator) `sudo apt install qtcreator qtmultimedia5-dev libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev`
1. Open `chatterino.pro` with QT Creator and build
## Arch Linux

View file

@ -103,6 +103,13 @@ To produce all supplement files for a standalone build, follow these steps (adju
cd C:\Users\example\src\build-chatterino-Desktop_Qt_5_11_2_MSVC2017_64bit-Release\release
C:\Qt\5.11.2\msvc2017_64\bin\windeployqt.exe chatterino.exe
5. The `releases` directory will now be populated with all the required files to make the chatterino build standalone.
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
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)).

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`
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,12 +1,3 @@
#-------------------------------------------------
#
# Project created by QtCreator 2016-12-28T18:23:35
#
#-------------------------------------------------
message(----)
# define project shit
QT += widgets core gui network multimedia svg concurrent
CONFIG += communi
COMMUNI += core model util
@ -34,7 +25,7 @@ equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") {
}
# Icons
#macx:ICON = resources/images/chatterino2.icns
macx:ICON = resources/chatterino.icns
win32:RC_FILE = resources/windows.rc
macx {
@ -59,31 +50,43 @@ include(lib/wintoast.pri)
SOURCES += \
src/Application.cpp \
src/autogenerated/ResourcesAutogen.cpp \
src/BrowserExtension.cpp \
src/common/Channel.cpp \
src/common/CompletionModel.cpp \
src/common/DownloadManager.cpp \
src/common/Env.cpp \
src/common/LinkParser.cpp \
src/common/NetworkData.cpp \
src/common/NetworkManager.cpp \
src/common/NetworkRequest.cpp \
src/common/NetworkResult.cpp \
src/common/NetworkTimer.cpp \
src/common/UsernameSet.cpp \
src/controllers/accounts/Account.cpp \
src/controllers/accounts/AccountController.cpp \
src/controllers/accounts/AccountModel.cpp \
src/controllers/commands/Command.cpp \
src/controllers/commands/CommandController.cpp \
src/controllers/commands/CommandModel.cpp \
src/controllers/highlights/HighlightBlacklistModel.cpp \
src/controllers/highlights/HighlightController.cpp \
src/controllers/highlights/HighlightModel.cpp \
src/controllers/highlights/HighlightBlacklistModel.cpp \
src/controllers/highlights/UserHighlightModel.cpp \
src/controllers/ignores/IgnoreController.cpp \
src/controllers/ignores/IgnoreModel.cpp \
src/controllers/moderationactions/ModerationAction.cpp \
src/controllers/moderationactions/ModerationActionModel.cpp \
src/controllers/moderationactions/ModerationActions.cpp \
src/controllers/notifications/NotificationController.cpp \
src/controllers/notifications/NotificationModel.cpp \
src/controllers/taggedusers/TaggedUser.cpp \
src/controllers/taggedusers/TaggedUsersController.cpp \
src/controllers/taggedusers/TaggedUsersModel.cpp \
src/main.cpp \
src/messages/Emote.cpp \
src/messages/Image.cpp \
src/messages/ImageSet.cpp \
src/messages/layouts/MessageLayout.cpp \
src/messages/layouts/MessageLayoutContainer.cpp \
src/messages/layouts/MessageLayoutElement.cpp \
@ -91,30 +94,57 @@ SOURCES += \
src/messages/Message.cpp \
src/messages/MessageBuilder.cpp \
src/messages/MessageColor.cpp \
src/messages/MessageContainer.cpp \
src/messages/MessageElement.cpp \
src/providers/bttv/BttvEmotes.cpp \
src/providers/bttv/LoadBttvChannelEmote.cpp \
src/providers/chatterino/ChatterinoBadges.cpp \
src/providers/emoji/Emojis.cpp \
src/providers/ffz/FfzEmotes.cpp \
src/providers/ffz/FfzModBadge.cpp \
src/providers/irc/AbstractIrcServer.cpp \
src/providers/irc/IrcAccount.cpp \
src/providers/irc/IrcChannel2.cpp \
src/providers/irc/IrcConnection2.cpp \
src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \
src/providers/twitch/ChatroomChannel.cpp \
src/providers/twitch/IrcMessageHandler.cpp \
src/providers/twitch/PartialTwitchUser.cpp \
src/providers/twitch/PubsubActions.cpp \
src/providers/twitch/PubsubClient.cpp \
src/providers/twitch/PubsubHelpers.cpp \
src/providers/twitch/TwitchAccount.cpp \
src/providers/twitch/TwitchAccountManager.cpp \
src/providers/twitch/TwitchApi.cpp \
src/providers/twitch/TwitchBadges.cpp \
src/providers/twitch/TwitchChannel.cpp \
src/providers/twitch/TwitchEmotes.cpp \
src/providers/twitch/TwitchHelpers.cpp \
src/providers/twitch/TwitchMessageBuilder.cpp \
src/providers/twitch/TwitchParseCheerEmotes.cpp \
src/providers/twitch/TwitchServer.cpp \
src/providers/twitch/TwitchUser.cpp \
src/RunGui.cpp \
src/singletons/Badges.cpp \
src/singletons/Emotes.cpp \
src/singletons/helper/GifTimer.cpp \
src/singletons/helper/LoggingChannel.cpp \
src/controllers/moderationactions/ModerationAction.cpp \
src/singletons/Logging.cpp \
src/singletons/NativeMessaging.cpp \
src/singletons/Paths.cpp \
src/singletons/Resources.cpp \
src/singletons/Settings.cpp \
src/singletons/Theme.cpp \
src/singletons/Toasts.cpp \
src/singletons/Updates.cpp \
src/singletons/WindowManager.cpp \
src/singletons/TooltipPreviewImage.cpp \
src/util/DebugCount.cpp \
src/util/FormatTime.cpp \
src/util/IncognitoBrowser.cpp \
src/util/InitUpdateButton.cpp \
src/util/JsonQuery.cpp \
src/util/RapidjsonHelpers.cpp \
src/util/StreamLink.cpp \
src/widgets/AccountSwitchPopupWidget.cpp \
@ -129,6 +159,7 @@ SOURCES += \
src/widgets/dialogs/SelectChannelDialog.cpp \
src/widgets/dialogs/SettingsDialog.cpp \
src/widgets/dialogs/TextInputDialog.cpp \
src/widgets/dialogs/UpdateDialog.cpp \
src/widgets/dialogs/UserInfoPopup.cpp \
src/widgets/dialogs/WelcomeDialog.cpp \
src/widgets/helper/ChannelView.cpp \
@ -145,17 +176,23 @@ SOURCES += \
src/widgets/Scrollbar.cpp \
src/widgets/settingspages/AboutPage.cpp \
src/widgets/settingspages/AccountsPage.cpp \
src/widgets/settingspages/AdvancedPage.cpp \
src/widgets/settingspages/BrowserExtensionPage.cpp \
src/widgets/settingspages/CommandPage.cpp \
src/widgets/settingspages/EmotesPage.cpp \
src/widgets/settingspages/ExternalToolsPage.cpp \
src/widgets/settingspages/FeelPage.cpp \
src/widgets/settingspages/GeneralPage.cpp \
src/widgets/settingspages/HighlightingPage.cpp \
src/widgets/settingspages/IgnoresPage.cpp \
src/widgets/settingspages/KeyboardSettingsPage.cpp \
src/widgets/settingspages/LogsPage.cpp \
src/widgets/settingspages/LookPage.cpp \
src/widgets/settingspages/ModerationPage.cpp \
src/widgets/settingspages/NotificationPage.cpp \
src/widgets/settingspages/SettingsPage.cpp \
src/widgets/settingspages/SpecialChannelsPage.cpp \
src/widgets/splits/ClosedSplits.cpp \
src/widgets/splits/Split.cpp \
src/widgets/splits/SplitContainer.cpp \
src/widgets/splits/SplitHeader.cpp \
@ -163,57 +200,21 @@ SOURCES += \
src/widgets/splits/SplitOverlay.cpp \
src/widgets/StreamView.cpp \
src/widgets/Window.cpp \
src/common/LinkParser.cpp \
src/controllers/moderationactions/ModerationActions.cpp \
src/singletons/NativeMessaging.cpp \
src/singletons/Emotes.cpp \
src/singletons/Logging.cpp \
src/singletons/Paths.cpp \
src/singletons/Resources.cpp \
src/singletons/Settings.cpp \
src/singletons/Updates.cpp \
src/singletons/Theme.cpp \
src/controllers/moderationactions/ModerationActionModel.cpp \
src/widgets/settingspages/LookPage.cpp \
src/widgets/settingspages/FeelPage.cpp \
src/util/InitUpdateButton.cpp \
src/widgets/dialogs/UpdateDialog.cpp \
src/widgets/settingspages/IgnoresPage.cpp \
src/providers/twitch/PubsubClient.cpp \
src/providers/twitch/TwitchApi.cpp \
src/messages/Emote.cpp \
src/messages/ImageSet.cpp \
src/providers/bttv/BttvEmotes.cpp \
src/providers/LinkResolver.cpp \
src/providers/ffz/FfzEmotes.cpp \
src/autogenerated/ResourcesAutogen.cpp \
src/singletons/Badges.cpp \
src/providers/twitch/TwitchBadges.cpp \
src/providers/chatterino/ChatterinoBadges.cpp \
src/providers/twitch/TwitchParseCheerEmotes.cpp \
src/providers/bttv/LoadBttvChannelEmote.cpp \
src/util/JsonQuery.cpp \
src/RunGui.cpp \
src/BrowserExtension.cpp \
src/util/FormatTime.cpp \
src/controllers/notifications/NotificationModel.cpp \
src/singletons/Toasts.cpp \
src/common/DownloadManager.cpp \
src/messages/MessageContainer.cpp \
src/common/UsernameSet.cpp \
src/widgets/settingspages/AdvancedPage.cpp \
src/util/IncognitoBrowser.cpp \
src/widgets/splits/ClosedSplits.cpp \
src/providers/ffz/FfzModBadge.cpp \
src/widgets/settingspages/GeneralPage.cpp \
src/providers/twitch/ChatroomChannel.cpp
src/controllers/pings/PingController.cpp \
src/controllers/pings/PingModel.cpp \
HEADERS += \
HEADERS += \
src/Application.hpp \
src/autogenerated/ResourcesAutogen.hpp \
src/BrowserExtension.hpp \
src/common/Aliases.hpp \
src/common/Atomic.hpp \
src/common/Channel.hpp \
src/common/Common.hpp \
src/common/CompletionModel.hpp \
src/common/Atomic.hpp \
src/common/ConcurrentMap.hpp \
src/common/DownloadManager.hpp \
src/common/LinkParser.hpp \
src/common/NetworkCommon.hpp \
src/common/NetworkData.hpp \
src/common/NetworkManager.hpp \
@ -224,7 +225,10 @@ HEADERS += \
src/common/NetworkWorker.hpp \
src/common/NullablePtr.hpp \
src/common/ProviderId.hpp \
src/common/SignalVector.hpp \
src/common/SignalVectorModel.hpp \
src/common/UniqueAccess.hpp \
src/common/UsernameSet.hpp \
src/common/Version.hpp \
src/controllers/accounts/Account.hpp \
src/controllers/accounts/AccountController.hpp \
@ -232,20 +236,26 @@ HEADERS += \
src/controllers/commands/Command.hpp \
src/controllers/commands/CommandController.hpp \
src/controllers/commands/CommandModel.hpp \
src/controllers/highlights/HighlightBlacklistModel.hpp \
src/controllers/highlights/HighlightBlacklistUser.hpp \
src/controllers/highlights/HighlightController.hpp \
src/controllers/highlights/HighlightModel.hpp \
src/controllers/highlights/HighlightBlacklistModel.hpp \
src/controllers/highlights/HighlightPhrase.hpp \
src/controllers/highlights/HighlightBlacklistUser.hpp \
src/controllers/highlights/UserHighlightModel.hpp \
src/controllers/ignores/IgnoreController.hpp \
src/controllers/ignores/IgnoreModel.hpp \
src/controllers/ignores/IgnorePhrase.hpp \
src/controllers/moderationactions/ModerationAction.hpp \
src/controllers/moderationactions/ModerationActionModel.hpp \
src/controllers/moderationactions/ModerationActions.hpp \
src/controllers/notifications/NotificationController.hpp \
src/controllers/notifications/NotificationModel.hpp \
src/controllers/taggedusers/TaggedUser.hpp \
src/controllers/taggedusers/TaggedUsersController.hpp \
src/controllers/taggedusers/TaggedUsersModel.hpp \
src/messages/Emote.hpp \
src/messages/Image.hpp \
src/messages/ImageSet.hpp \
src/messages/layouts/MessageLayout.hpp \
src/messages/layouts/MessageLayoutContainer.hpp \
src/messages/layouts/MessageLayoutElement.hpp \
@ -255,37 +265,68 @@ HEADERS += \
src/messages/Message.hpp \
src/messages/MessageBuilder.hpp \
src/messages/MessageColor.hpp \
src/messages/MessageContainer.hpp \
src/messages/MessageElement.hpp \
src/messages/MessageParseArgs.hpp \
src/messages/Selection.hpp \
src/PrecompiledHeader.hpp \
src/providers/bttv/BttvEmotes.hpp \
src/providers/bttv/LoadBttvChannelEmote.hpp \
src/providers/chatterino/ChatterinoBadges.hpp \
src/providers/emoji/Emojis.hpp \
src/providers/ffz/FfzEmotes.hpp \
src/providers/ffz/FfzModBadge.hpp \
src/providers/irc/AbstractIrcServer.hpp \
src/providers/irc/IrcAccount.hpp \
src/providers/irc/IrcChannel2.hpp \
src/providers/irc/IrcConnection2.hpp \
src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \
src/providers/twitch/ChatroomChannel.hpp \
src/providers/twitch/EmoteValue.hpp \
src/providers/twitch/IrcMessageHandler.hpp \
src/providers/twitch/PartialTwitchUser.hpp \
src/providers/twitch/PubsubActions.hpp \
src/providers/twitch/PubsubClient.hpp \
src/providers/twitch/PubsubHelpers.hpp \
src/providers/twitch/TwitchAccount.hpp \
src/providers/twitch/TwitchAccountManager.hpp \
src/providers/twitch/TwitchApi.hpp \
src/providers/twitch/TwitchBadges.hpp \
src/providers/twitch/TwitchChannel.hpp \
src/providers/twitch/TwitchCommon.hpp \
src/providers/twitch/TwitchEmotes.hpp \
src/providers/twitch/TwitchHelpers.hpp \
src/providers/twitch/TwitchMessageBuilder.hpp \
src/providers/twitch/TwitchParseCheerEmotes.hpp \
src/providers/twitch/TwitchServer.hpp \
src/providers/twitch/TwitchUser.hpp \
src/RunGui.hpp \
src/singletons/TooltipPreviewImage.hpp \
src/singletons/Badges.hpp \
src/singletons/Emotes.hpp \
src/singletons/helper/GifTimer.hpp \
src/singletons/helper/LoggingChannel.hpp \
src/controllers/moderationactions/ModerationAction.hpp \
src/singletons/Logging.hpp \
src/singletons/NativeMessaging.hpp \
src/singletons/Paths.hpp \
src/singletons/Resources.hpp \
src/singletons/Settings.hpp \
src/singletons/Theme.hpp \
src/singletons/Toasts.hpp \
src/singletons/Updates.hpp \
src/singletons/WindowManager.hpp \
src/util/ConcurrentMap.hpp \
src/util/DebugCount.hpp \
src/util/FormatTime.hpp \
src/util/IncognitoBrowser.hpp \
src/util/InitUpdateButton.hpp \
src/util/IrcHelpers.hpp \
src/util/IsBigEndian.hpp \
src/util/JsonQuery.hpp \
src/util/LayoutCreator.hpp \
src/util/QStringHash.hpp \
src/util/rangealgorithm.hpp \
src/util/RapidjsonHelpers.hpp \
src/util/RemoveScrollAreaBackground.hpp \
src/util/SharedPtrElementLess.hpp \
@ -303,10 +344,12 @@ HEADERS += \
src/widgets/dialogs/SelectChannelDialog.hpp \
src/widgets/dialogs/SettingsDialog.hpp \
src/widgets/dialogs/TextInputDialog.hpp \
src/widgets/dialogs/UpdateDialog.hpp \
src/widgets/dialogs/UserInfoPopup.hpp \
src/widgets/dialogs/WelcomeDialog.hpp \
src/widgets/helper/ChannelView.hpp \
src/widgets/helper/ComboBoxItemDelegate.hpp \
src/widgets/helper/CommonTexts.hpp \
src/widgets/helper/DebugPopup.hpp \
src/widgets/helper/EditableModelView.hpp \
src/widgets/helper/Line.hpp \
@ -320,17 +363,23 @@ HEADERS += \
src/widgets/Scrollbar.hpp \
src/widgets/settingspages/AboutPage.hpp \
src/widgets/settingspages/AccountsPage.hpp \
src/widgets/settingspages/AdvancedPage.hpp \
src/widgets/settingspages/BrowserExtensionPage.hpp \
src/widgets/settingspages/CommandPage.hpp \
src/widgets/settingspages/EmotesPage.hpp \
src/widgets/settingspages/ExternalToolsPage.hpp \
src/widgets/settingspages/FeelPage.hpp \
src/widgets/settingspages/GeneralPage.hpp \
src/widgets/settingspages/HighlightingPage.hpp \
src/widgets/settingspages/IgnoresPage.hpp \
src/widgets/settingspages/KeyboardSettingsPage.hpp \
src/widgets/settingspages/LogsPage.hpp \
src/widgets/settingspages/LookPage.hpp \
src/widgets/settingspages/ModerationPage.hpp \
src/widgets/settingspages/NotificationPage.hpp \
src/widgets/settingspages/SettingsPage.hpp \
src/widgets/settingspages/SpecialChannelsPage.hpp \
src/widgets/splits/ClosedSplits.hpp \
src/widgets/splits/Split.hpp \
src/widgets/splits/SplitContainer.hpp \
src/widgets/splits/SplitHeader.hpp \
@ -338,56 +387,8 @@ HEADERS += \
src/widgets/splits/SplitOverlay.hpp \
src/widgets/StreamView.hpp \
src/widgets/Window.hpp \
src/providers/twitch/TwitchCommon.hpp \
src/util/IsBigEndian.hpp \
src/common/LinkParser.hpp \
src/controllers/moderationactions/ModerationActions.hpp \
src/singletons/Emotes.hpp \
src/singletons/Logging.hpp \
src/singletons/Paths.hpp \
src/singletons/Resources.hpp \
src/singletons/Settings.hpp \
src/singletons/Updates.hpp \
src/singletons/NativeMessaging.hpp \
src/singletons/Theme.hpp \
src/common/SignalVector.hpp \
src/widgets/dialogs/LogsPopup.hpp \
src/controllers/moderationactions/ModerationActionModel.hpp \
src/widgets/settingspages/LookPage.hpp \
src/widgets/settingspages/FeelPage.hpp \
src/util/InitUpdateButton.hpp \
src/widgets/dialogs/UpdateDialog.hpp \
src/widgets/settingspages/IgnoresPage.hpp \
src/providers/twitch/PubsubClient.hpp \
src/providers/twitch/TwitchApi.hpp \
src/messages/Emote.hpp \
src/messages/ImageSet.hpp \
src/providers/bttv/BttvEmotes.hpp \
src/providers/LinkResolver.hpp \
src/providers/ffz/FfzEmotes.hpp \
src/autogenerated/ResourcesAutogen.hpp \
src/singletons/Badges.hpp \
src/providers/twitch/TwitchBadges.hpp \
src/providers/chatterino/ChatterinoBadges.hpp \
src/common/Aliases.hpp \
src/providers/twitch/TwitchParseCheerEmotes.hpp \
src/providers/bttv/LoadBttvChannelEmote.hpp \
src/util/JsonQuery.hpp \
src/RunGui.hpp \
src/BrowserExtension.hpp \
src/util/FormatTime.hpp \
src/controllers/notifications/NotificationModel.hpp \
src/singletons/Toasts.hpp \
src/common/DownloadManager.hpp \
src/messages/MessageContainer.hpp \
src/common/UsernameSet.hpp \
src/widgets/settingspages/AdvancedPage.hpp \
src/util/IncognitoBrowser.hpp \
src/widgets/splits/ClosedSplits.hpp \
src/providers/ffz/FfzModBadge.hpp \
src/widgets/settingspages/GeneralPage.hpp \
src/messages/HistoricMessageAppearance.hpp \
src/providers/twitch/ChatroomChannel.hpp
src/controllers/pings/PingController.hpp \
src/controllers/pings/PingModel.hpp \
RESOURCES += \
resources/resources.qrc \
@ -401,3 +402,23 @@ FORMS +=
#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.
# this will ignore a/b/c/d.txt and a/b/xd.txt
ignored_directories = ['__pycache__']
ignored_directories = ['__pycache__', 'linuxinstall']
def isNotIgnored(file):
# check if file exists in an ignored direcory

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 &&
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)

View file

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

View file

@ -6,11 +6,13 @@
#include <unordered_map>
namespace chatterino {
class Prefix
{
public:
Prefix(const QString &string);
bool operator==(const Prefix &other) const;
bool operator!=(const Prefix &other) const;
bool isStartOf(const QString &string) const;
private:
@ -19,9 +21,11 @@ private:
friend struct std::hash<Prefix>;
};
} // namespace chatterino
namespace std {
template <>
struct hash<chatterino::Prefix> {
size_t operator()(const chatterino::Prefix &prefix) const
@ -30,9 +34,18 @@ struct hash<chatterino::Prefix> {
size_t(prefix.second.unicode());
}
};
} // namespace std
namespace chatterino {
struct CaseInsensitiveLess {
bool operator()(const QString &lhs, const QString &rhs) const
{
return lhs.compare(rhs, Qt::CaseInsensitive) < 0;
}
};
class UsernameSet
{
public:
@ -66,7 +79,7 @@ public:
private:
void insertPrefix(const QString &string);
std::set<QString> items;
std::set<QString, CaseInsensitiveLess> items;
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)
{
auto row = this->createRow();
auto newRow = this->createRow();
setStringItem(row[0], item->getCategory(), false, false);
row[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole);
setStringItem(newRow[0], item->getCategory(), false, false);
newRow[0]->setData(QFont("Segoe UI Light", 16), Qt::FontRole);
this->insertCustomRow(std::move(row), proposedIndex);
this->insertCustomRow(std::move(newRow), proposedIndex);
return proposedIndex + 1;
}

View file

@ -15,6 +15,7 @@
#include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/CombinePath.hpp"
#include "widgets/dialogs/LogsPopup.hpp"
@ -28,9 +29,143 @@
"/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
"/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
"/clear", "/subscribers", "/subscribersoff", "/followers", \
"/followersoff" \
"/followersoff", "/user" \
}
namespace {
using namespace chatterino;
static const QStringList whisperCommands{"/w", ".w"};
void sendWhisperMessage(const QString &text)
{
// (hemirt) pajlada: "we should not be sending whispers through jtv, but
// rather to your own username"
auto app = getApp();
app->twitch.server->sendMessage("jtv", text.simplified());
}
bool appendWhisperMessageWordsLocally(const QStringList &words)
{
auto app = getApp();
MessageBuilder b;
b.emplace<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 {
void CommandController::initialize(Settings &, Paths &paths)
@ -122,99 +257,12 @@ QString CommandController::execCommand(const QString &textNoEmoji,
// works in a valid twitch channel and /whispers, etc...
if (!dryRun && channel->isTwitchChannel())
{
if (commandName == "/w")
if (whisperCommands.contains(commandName, Qt::CaseInsensitive))
{
if (words.length() <= 2)
if (words.length() > 2)
{
return "";
}
auto app = getApp();
MessageBuilder b;
b.emplace<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);
});
appendWhisperMessageWordsLocally(words);
sendWhisperMessage(text);
}
return "";
@ -413,13 +461,36 @@ QString CommandController::execCommand(const QString &textNoEmoji,
logs->show();
return "";
}
else if (commandName == "/user")
{
if (words.size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage /user [user] (channel)"));
return "";
}
QString channelName = channel->getName();
if (words.size() > 2)
{
channelName = words[2];
if (channelName[0] == '#')
{
channelName.remove(0, 1);
}
}
QDesktopServices::openUrl("https://www.twitch.tv/popout/" +
channelName + "/viewercard/" + words[1]);
return "";
}
}
// check if custom command exists
auto it = this->commandsMap_.find(commandName);
if (it != this->commandsMap_.end())
{
return this->execCustomCommand(words, it.value());
// check if custom command exists
const auto it = this->commandsMap_.find(commandName);
if (it != this->commandsMap_.end())
{
return this->execCustomCommand(words, it.value(), dryRun);
}
}
auto maxSpaces = std::min(this->maxSpaces_, words.length() - 1);
@ -427,10 +498,10 @@ QString CommandController::execCommand(const QString &textNoEmoji,
{
commandName += ' ' + words[i + 1];
auto it = this->commandsMap_.find(commandName);
const auto it = this->commandsMap_.find(commandName);
if (it != this->commandsMap_.end())
{
return this->execCustomCommand(words, it.value());
return this->execCustomCommand(words, it.value(), dryRun);
}
}
@ -438,7 +509,8 @@ QString CommandController::execCommand(const QString &textNoEmoji,
}
QString CommandController::execCustomCommand(const QStringList &words,
const Command &command)
const Command &command,
bool dryRun)
{
QString result;
@ -509,7 +581,17 @@ QString CommandController::execCustomCommand(const QStringList &words,
result = result.mid(1);
}
return result.replace("{{", "{");
auto res = result.replace("{{", "{");
if (dryRun || !appendWhisperMessageStringLocally(res))
{
return res;
}
else
{
sendWhisperMessage(res);
return "";
}
}
QStringList CommandController::getDefaultTwitchCommandList()

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -5,6 +5,7 @@
#include "messages/Image.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
#include "providers/LinkResolver.hpp"
#include "providers/twitch/PubsubActions.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
@ -64,6 +65,8 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
builder = MessageBuilder();
builder.emplace<TimestampElement>();
builder.emplace<TwitchModerationElement>();
builder.message().loginName = action.target.name;
builder.message().flags.set(MessageFlag::PubSub);
builder
@ -90,23 +93,30 @@ MessageBuilder::MessageBuilder()
{
}
MessageBuilder::MessageBuilder(const QString &text)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
this->message().searchText = text;
}
MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
: MessageBuilder()
{
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::DoNotTriggerNotification);
this->message().messageText = text;
this->message().searchText = text;
}
@ -157,6 +167,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
this->emplace<TimestampElement>();
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
this->message().messageText = text;
this->message().searchText = text;
}
@ -213,6 +224,7 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
this->message().messageText = text;
this->message().searchText = text;
}
@ -225,23 +237,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
this->message().timeoutUser = action.target.name;
QString text;
if (action.wasBan())
{
text = QString("%1 unbanned %2.") //
.arg(action.source.name)
.arg(action.target.name);
}
else
{
text = QString("%1 untimedout %2.") //
.arg(action.source.name)
.arg(action.target.name);
}
QString text =
QString("%1 %2 %3.")
.arg(action.source.name)
.arg(QString(action.wasBan() ? "unbanned" : "untimedout"))
.arg(action.target.name);
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
this->message().messageText = text;
this->message().searchText = text;
}
@ -352,4 +356,57 @@ QString MessageBuilder::matchLink(const QString &string)
return captured;
}
void MessageBuilder::addLink(const QString &origLink,
const QString &matchedLink)
{
static QRegularExpression domainRegex(
R"(^(?:(?:ftp|http)s?:\/\/)?([^\/]+)(?:\/.*)?$)",
QRegularExpression::CaseInsensitiveOption);
QString lowercaseLinkString;
auto match = domainRegex.match(origLink);
if (match.isValid())
{
lowercaseLinkString = origLink.mid(0, match.capturedStart(1)) +
match.captured(1).toLower() +
origLink.mid(match.capturedEnd(1));
}
else
{
lowercaseLinkString = origLink;
}
auto linkElement = Link(Link::Url, matchedLink);
auto textColor = MessageColor(MessageColor::Link);
auto linkMELowercase =
this->emplace<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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,7 +14,7 @@ class QPainter;
namespace chatterino {
enum class MessageFlag : uint16_t;
enum class MessageFlag : uint32_t;
using MessageFlags = FlagsEnum<MessageFlag>;
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

View file

@ -41,6 +41,7 @@ public:
virtual void paintAnimated(QPainter &painter, int yOffset) = 0;
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
virtual int getXFromIndex(int index) = 0;
const Link &getLink() const;
const QString &getText() const;
FlagsEnum<MessageElementFlag> getFlags() const;
@ -125,25 +126,4 @@ private:
QString line2;
};
class TestLayoutElement : public MessageLayoutElement
{
public:
TestLayoutElement(MessageElement &creator, const QSize &size,
const QColor &background, bool end);
protected:
void addCopyTextToString(QString &str, int from = 0,
int to = INT_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
private:
QSize size_;
QColor background_;
bool end_;
};
} // namespace chatterino

View file

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

View file

@ -36,9 +36,9 @@ namespace {
else
{
const auto &shortCodes = unparsedEmoji["short_names"];
for (const auto &shortCode : shortCodes.GetArray())
for (const auto &_shortCode : shortCodes.GetArray())
{
emojiData->shortCodes.emplace_back(shortCode.GetString());
emojiData->shortCodes.emplace_back(_shortCode.GetString());
}
}
@ -240,8 +240,6 @@ void Emojis::sortEmojis()
void Emojis::loadEmojiSet()
{
auto app = getApp();
getSettings()->emojiSet.connect([=](const auto &emojiSet) {
this->emojis.each([=](const auto &name,
std::shared_ptr<EmojiData> &emoji) {

View file

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

View file

@ -20,12 +20,69 @@
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()
{
static IrcMessageHandler 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,
TwitchServer &server)
{
@ -203,28 +260,78 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
// refresh all
app->windows->repaintVisibleChatWidgets(chan.get());
if (getSettings()->hideModerated)
{
app->windows->forceLayoutChannelViews();
}
}
void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
{
// check parameter count
if (message->parameters().length() < 1)
{
return;
}
QString chanName;
if (!trimChannelName(message->parameter(0), chanName))
{
return;
}
auto app = getApp();
// get channel
auto chan = app->twitch.server->getChannelOrEmpty(chanName);
if (chan->isEmpty())
{
log("[IrcMessageHandler:handleClearMessageMessage] Twitch channel {} "
"not "
"found",
chanName);
return;
}
auto tags = message->tags();
QString targetID = tags.value("target-msg-id").toString();
chan->deleteMessage(targetID);
}
void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
{
QVariant _mod = message->tag("mod");
auto app = getApp();
QString channelName;
if (!trimChannelName(message->parameter(0), channelName))
{
return;
}
auto c = app->twitch.server->getChannelOrEmpty(channelName);
if (c->isEmpty())
{
return;
}
QVariant _badges = message->tag("badges");
if (_badges.isValid())
{
TwitchChannel *tc = dynamic_cast<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())
{
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());
if (tc != nullptr)
{
@ -248,6 +355,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
if (!builder.isIgnored())
{
builder->flags.set(MessageFlag::Whisper);
MessagePtr _message = builder.build();
app->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
@ -273,6 +381,56 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
}
}
std::vector<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,
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)
{
auto app = getApp();
MessagePtr msg = makeSystemMessage(message->content());
auto builtMessages = this->parseNoticeMessage(message);
QString channelName;
if (!trimChannelName(message->target(), channelName))
for (auto msg : builtMessages)
{
// Notice wasn't targeted at a single channel, send to all twitch
// channels
app->twitch.server->forEachChannelAndSpecialChannels(
[msg](const auto &c) {
c->addMessage(msg); //
});
QString channelName;
if (!trimChannelName(message->target(), channelName) ||
channelName == "jtv")
{
// Notice wasn't targeted at a single channel, send to all twitch
// channels
app->twitch.server->forEachChannelAndSpecialChannels(
[msg](const auto &c) {
c->addMessage(msg); //
});
return;
return;
}
auto channel = app->twitch.server->getChannelOrEmpty(channelName);
if (channel->isEmpty())
{
log("[IrcManager:handleNoticeMessage] Channel {} not found in "
"channel "
"manager ",
channelName);
return;
}
QString tags = message->tags().value("msg-id", "").toString();
if (tags == "bad_delete_message_error" || tags == "usage_delete")
{
channel->addMessage(makeSystemMessage(
"Usage: \"/delete <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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -7,10 +7,21 @@ namespace chatterino {
enum class Platform : uint8_t;
enum class ToastReaction {
OpenInBrowser = 0,
OpenInPlayer = 1,
OpenInStreamlink = 2,
DontOpen = 3
};
class Toasts final : public Singleton
{
public:
void sendChannelNotification(const QString &channelName, Platform p);
static QString findStringFromReaction(const ToastReaction &reaction);
static QString findStringFromReaction(
const pajlada::Settings::Setting<int> &reaction);
static std::map<ToastReaction, QString> reactionToString;
static bool isEnabled();
@ -18,6 +29,7 @@ private:
#ifdef Q_OS_WIN
void sendWindowsNotification(const QString &channelName, Platform p);
#endif
static void fetchChannelAvatar(
const QString channelName,
std::function<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 <QJsonDocument>
#include <QJsonObject>
#include <QSaveFile>
#include <chrono>
@ -75,10 +76,7 @@ WindowManager::WindowManager()
this->wordFlagsListener_.addSetting(settings->showBadgesSubscription);
this->wordFlagsListener_.addSetting(settings->showBadgesVanity);
this->wordFlagsListener_.addSetting(settings->showBadgesChatterino);
this->wordFlagsListener_.addSetting(settings->enableBttvEmotes);
this->wordFlagsListener_.addSetting(settings->enableEmojis);
this->wordFlagsListener_.addSetting(settings->enableFfzEmotes);
this->wordFlagsListener_.addSetting(settings->enableTwitchEmotes);
this->wordFlagsListener_.addSetting(settings->enableEmoteImages);
this->wordFlagsListener_.addSetting(settings->boldUsernames);
this->wordFlagsListener_.addSetting(settings->lowercaseDomains);
this->wordFlagsListener_.setCB([this] {
@ -114,13 +112,12 @@ void WindowManager::updateWordTypeMask()
}
// emotes
flags.set(settings->enableTwitchEmotes ? MEF::TwitchEmoteImage
: MEF::TwitchEmoteText);
flags.set(settings->enableFfzEmotes ? MEF::FfzEmoteImage
: MEF::FfzEmoteText);
flags.set(settings->enableBttvEmotes ? MEF::BttvEmoteImage
: MEF::BttvEmoteText);
flags.set(settings->enableEmojis ? MEF::EmojiImage : MEF::EmojiText);
if (settings->enableEmoteImages)
{
flags.set(MEF::EmoteImages);
}
flags.set(MEF::EmoteText);
flags.set(MEF::EmojiText);
// bits
flags.set(MEF::BitsAmount);
@ -258,11 +255,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
// load file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QFile file(settingsPath);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
QJsonArray windows_arr = document.object().value("windows").toArray();
QJsonArray windows_arr = this->loadWindowArray(settingsPath);
// "deserialize"
for (QJsonValue window_val : windows_arr)
@ -390,10 +383,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
void WindowManager::save()
{
log("[WindowManager] Saving");
assertInGuiThread();
auto app = getApp();
QJsonDocument document;
// "serialize"
@ -477,7 +467,7 @@ void WindowManager::save()
// save file
QString settingsPath = getPaths()->settingsDirectory + SETTINGS_FILENAME;
QFile file(settingsPath);
QSaveFile file(settingsPath);
file.open(QIODevice::WriteOnly | QIODevice::Truncate);
QJsonDocument::JsonFormat format =
@ -489,7 +479,7 @@ void WindowManager::save()
;
file.write(document.toJson(format));
file.flush();
file.commit();
}
void WindowManager::sendAlert()
@ -516,6 +506,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj)
case SplitNode::_Split:
{
obj.insert("type", "split");
obj.insert("moderationMode", node->getSplit()->getModerationMode());
QJsonObject split;
encodeChannel(node->getSplit()->getIndirectChannel(), split);
obj.insert("data", split);
@ -621,4 +612,14 @@ void WindowManager::incGeneration()
this->generation_++;
}
QJsonArray WindowManager::loadWindowArray(const QString &settingsPath)
{
QFile file(settingsPath);
file.open(QIODevice::ReadOnly);
QByteArray data = file.readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
QJsonArray windows_arr = document.object().value("windows").toArray();
return windows_arr;
}
} // namespace chatterino

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@
#include <QShortcut>
#include <QVBoxLayout>
#include <QMenuBar>
#include <QStandardItemModel>
namespace chatterino {
@ -43,6 +44,10 @@ Window::Window(WindowType type)
this->addShortcuts();
this->addLayout();
#ifdef Q_OS_MACOS
this->addMenuBar();
#endif
this->signalHolder_.managedConnect(
getApp()->accounts->twitch.currentUserChanged,
[this] { this->onAccountSelected(); });
@ -336,6 +341,18 @@ void Window::addShortcuts()
});
}
void Window::addMenuBar()
{
QMenuBar *mainMenu = new QMenuBar();
mainMenu->setNativeMenuBar(true);
QMenu *menu = new QMenu(QString());
mainMenu->addMenu(menu);
QAction *prefs = menu->addAction(QString());
prefs->setMenuRole(QAction::PreferencesRole);
connect(prefs, &QAction::triggered, this, [] { SettingsDialog::showDialog(); });
}
#define UGLYMACROHACK1(s) #s
#define UGLYMACROHACK(s) UGLYMACROHACK1(s)

View file

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

View file

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

View file

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

View file

@ -15,6 +15,7 @@
#include "providers/twitch/TwitchServer.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp"
#include "singletons/WindowManager.hpp"
#include "util/DistanceBetweenPoints.hpp"
#include "util/IncognitoBrowser.hpp"
@ -294,8 +295,12 @@ void ChannelView::scaleChangedEvent(float scale)
if (this->goToBottom_)
{
auto factor = this->qtFontScale();
#ifdef Q_OS_MACOS
factor = scale * 80.f / this->logicalDpiX() * this->devicePixelRatioF();
#endif
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->update();
// this->updateTimer.start();
@ -1213,6 +1219,28 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
}
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->setWordWrap(isLinkValid);
tooltipWidget->setText(tooltip);
@ -1667,7 +1695,12 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
case Link::UserAction:
{
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);
}
break;

View file

@ -4,6 +4,7 @@
#include "messages/LimitedQueue.hpp"
#include "messages/LimitedQueueSnapshot.hpp"
#include "messages/Selection.hpp"
#include "messages/Image.hpp"
#include "widgets/BaseWidget.hpp"
#include <QPaintEvent>
@ -25,7 +26,7 @@ using ChannelPtr = std::shared_ptr<Channel>;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
enum class MessageFlag : uint16_t;
enum class MessageFlag : uint32_t;
using MessageFlags = FlagsEnum<MessageFlag>;
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)
, menu_(this)
{
auto app = getApp();
this->setAcceptDrops(true);
this->positionChangedAnimation_.setEasingCurve(
@ -527,8 +525,6 @@ void NotebookTab::dragEnterEvent(QDragEnterEvent *event)
void NotebookTab::mouseMoveEvent(QMouseEvent *event)
{
auto app = getApp();
if (getSettings()->showTabCloseButton &&
this->notebook_->getAllowUserTabManagement()) //
{

View file

@ -98,7 +98,7 @@ void ResizingTextEdit::keyPressEvent(QKeyEvent *event)
QString currentCompletionPrefix = this->textUnderCursor();
// check if there is something to complete
if (!currentCompletionPrefix.size())
if (currentCompletionPrefix.size() <= 1)
{
return;
}
@ -228,6 +228,27 @@ void ResizingTextEdit::insertCompletion(const QString &completion)
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
{
return this->completer_;

View file

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

View file

@ -17,6 +17,25 @@ SearchPopup::SearchPopup()
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()
{
// 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()
{
QString text = searchInput_->text();

View file

@ -22,6 +22,9 @@ public:
void setChannel(std::shared_ptr<Channel> channel);
protected:
void keyPressEvent(QKeyEvent *e) override;
private:
void initLayout();
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>("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);
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
}

View file

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

View file

@ -14,8 +14,6 @@ namespace chatterino {
ExternalToolsPage::ExternalToolsPage()
: SettingsPage("External tools", ":/settings/externaltools.svg")
{
auto app = getApp();
LayoutCreator<ExternalToolsPage> layoutCreator(this);
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 input when empty", s.showEmptyInput);
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.addCheckbox("Timestamps", s.showTimestamps);
@ -197,8 +200,8 @@ void GeneralPage::initLayout(SettingsLayout &layout)
s.timestampFormat, true);
layout.addDropdown<int>(
"Collapse messages",
{"Never", "Longer than 2 lines", "Longer than 3 lines",
"Longer than 4 lines", "Longer than 5 lines"},
{"Never", "After 2 lines", "After 3 lines", "After 4 lines",
"After 5 lines"},
s.collpseMessagesMinLines,
[](auto val) {
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("Mark last message you read");
// 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.addDropdown<float>(
@ -223,6 +228,7 @@ void GeneralPage::initLayout(SettingsLayout &layout)
[](auto args) { return fuzzyToFloat(args.value, 1.f); });
layout.addCheckbox("Gif animations", s.animateEmotes);
layout.addCheckbox("Animate only when focused", s.animationsWhenFocused);
layout.addCheckbox("Emote images", s.enableEmoteImages);
layout.addDropdown("Emoji set",
{"EmojiOne 2", "EmojiOne 3", "Twitter", "Facebook",
"Apple", "Google", "Messenger"},
@ -249,63 +255,35 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addTitle("Miscellaneous");
layout.addCheckbox("Show joined users (< 1000 chatters)", s.showJoins);
layout.addCheckbox("Show parted users (< 1000 chatters)", s.showParts);
layout.addDropdown("Boldness", {"Not implemented"});
layout.addCheckbox("Lowercase domains", s.lowercaseDomains);
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("Double click links to open", s.linksDoubleClickOnly);
layout.addCheckbox("Unshorten links", s.unshortLinks);
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.addSeperator();
layout.addTitle2("Misc");
layout.addTitle2("Miscellaneous (Twitch)");
layout.addCheckbox("Show twitch whispers inline", s.inlineWhispers);
layout.addDropdown<int>(
"Historic messages appearance",
{"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.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect);
/*
layout.addTitle2("Cache");

View file

@ -77,8 +77,24 @@ void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> users,
auto anyways = users.emplace<QHBoxLayout>().withoutMargin();
{
anyways.emplace<QLabel>("Show anyways if:");
anyways.emplace<QComboBox>();
anyways.emplace<QLabel>("Show messages from ignored users anyways:");
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);
}

View file

@ -43,7 +43,8 @@ KeyboardSettingsPage::KeyboardSettingsPage()
new QLabel("Search in current channel"));
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("F5"), new QLabel("Reload subscriber and channel emotes"));
form->addRow(new QLabel("F5"),
new QLabel("Reload subscriber and channel emotes"));
}
} // namespace chatterino

View file

@ -182,12 +182,6 @@ void LookPage::addMessageTab(LayoutCreator<QVBoxLayout> layout)
layout.append(
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);
// bold-slider

View file

@ -61,15 +61,10 @@ QString formatSize(qint64 size)
QString fetchLogDirectorySize()
{
QString logPathDirectory;
if (getSettings()->logPath == "")
{
logPathDirectory = getPaths()->messageLogDirectory;
}
else
{
logPathDirectory = getSettings()->logPath;
}
QString logPathDirectory = getSettings()->logPath.getValue().isEmpty()
? getPaths()->messageLogDirectory
: getSettings()->logPath;
qint64 logsSize = dirSize(logPathDirectory);
QString logsSizeLabel = "Your logs currently take up ";
logsSizeLabel += formatSize(logsSize);
@ -96,31 +91,22 @@ ModerationPage::ModerationPage()
QtConcurrent::run([] { return fetchLogDirectorySize(); }));
// Logs (copied from LoggingMananger)
getSettings()->logPath.connect(
[logsPathLabel](const QString &logPath, auto) mutable {
QString pathOriginal;
getSettings()->logPath.connect([logsPathLabel](const QString &logPath,
auto) mutable {
QString pathOriginal =
logPath.isEmpty() ? getPaths()->messageLogDirectory : logPath;
if (logPath == "")
{
pathOriginal = getPaths()->messageLogDirectory;
}
else
{
pathOriginal = logPath;
}
QString pathShortened =
"Logs are saved at <a href=\"file:///" + pathOriginal +
"\"><span style=\"color: white;\">" +
shortenString(pathOriginal, 50) + "</span></a>";
QString pathShortened =
"Logs are saved at <a href=\"file:///" + pathOriginal +
"\"><span style=\"color: white;\">" +
shortenString(pathOriginal, 50) + "</span></a>";
logsPathLabel->setText(pathShortened);
logsPathLabel->setToolTip(pathOriginal);
});
logsPathLabel->setText(pathShortened);
logsPathLabel->setToolTip(pathOriginal);
});
logsPathLabel->setTextFormat(Qt::RichText);
logsPathLabel->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard |
Qt::LinksAccessibleByKeyboard);
logsPathLabel->setOpenExternalLinks(true);
logs.append(this->createCheckBox("Enable logging",
@ -161,7 +147,8 @@ ModerationPage::ModerationPage()
// clang-format off
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 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->setStyleSheet("color: #bbb");
// clang-format on

View file

@ -4,6 +4,7 @@
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/notifications/NotificationModel.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Toasts.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/helper/EditableModelView.hpp"
@ -23,7 +24,6 @@ NotificationPage::NotificationPage()
: SettingsPage("Notifications", ":/settings/notification2.svg")
{
LayoutCreator<NotificationPage> layoutCreator(this);
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
{
auto tabs = layout.emplace<QTabWidget>();
@ -39,6 +39,23 @@ NotificationPage::NotificationPage()
settings.append(
this->createCheckBox("Enable toasts (Windows 8 or later)",
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
auto customSound =
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

View file

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

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