Merge branch 'master' of github.com:Chatterino/chatterino2 into chore/eradicate_hardcoded_names_from_our_amazing_codebase

This commit is contained in:
Mm2PL 2023-08-06 18:15:34 +02:00
commit d38caafcbc
222 changed files with 8851 additions and 3057 deletions

47
.CI/build-installer.ps1 Normal file
View file

@ -0,0 +1,47 @@
if (-not (Test-Path -PathType Container Chatterino2)) {
Write-Error "Couldn't find a folder called 'Chatterino2' in the current directory.";
exit 1
}
# Check if we're on a tag
$OldErrorActionPref = $ErrorActionPreference;
$ErrorActionPreference = 'Continue';
git describe --exact-match --match 'v*' *> $null;
$isTagged = $?;
$ErrorActionPreference = $OldErrorActionPref;
$defines = $null;
if ($isTagged) {
# This is a release.
# Make sure, any existing `modes` file is overwritten for the user,
# for example when updating from nightly to stable.
Write-Output "" > Chatterino2/modes;
$installerBaseName = "Chatterino.Installer";
}
else {
Write-Output nightly > Chatterino2/modes;
$defines = "/DIS_NIGHTLY=1";
$installerBaseName = "Chatterino.Nightly.Installer";
}
if ($Env:GITHUB_OUTPUT) {
# This is used in CI when creating the artifact
"C2_INSTALLER_BASE_NAME=$installerBaseName" >> "$Env:GITHUB_OUTPUT"
}
# Copy vc_redist.x64.exe
if ($null -eq $Env:VCToolsRedistDir) {
Write-Error "VCToolsRedistDir is not set. Forgot to set Visual Studio environment variables?";
exit 1
}
Copy-Item "$Env:VCToolsRedistDir\vc_redist.x64.exe" .;
# Build the installer
ISCC `
/DWORKING_DIR="$($pwd.Path)\" `
/DINSTALLER_BASE_NAME="$installerBaseName" `
$defines `
/O. `
"$PSScriptRoot\chatterino-installer.iss";
Move-Item "$installerBaseName.exe" "$installerBaseName$($Env:VARIANT_SUFFIX).exe"

View file

@ -0,0 +1,87 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Chatterino"
#define MyAppVersion "2.4.4"
#define MyAppPublisher "Chatterino Team"
#define MyAppURL "https://www.chatterino.com"
#define MyAppExeName "chatterino.exe"
; used in build-installer.ps1
; if set, must end in a backslash
#ifndef WORKING_DIR
#define WORKING_DIR ""
#endif
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{F5FE6614-04D4-4D32-8600-0ABA0AC113A4}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
VersionInfoVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
ArchitecturesInstallIn64BitMode=x64
;Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputDir=out
; This is defined by the build-installer.ps1 script,
; but kept optional for regular use.
#ifdef INSTALLER_BASE_NAME
OutputBaseFilename={#INSTALLER_BASE_NAME}
#else
OutputBaseFilename=Chatterino.Installer
#endif
Compression=lzma
SolidCompression=yes
WizardStyle=modern
UsePreviousTasks=no
UninstallDisplayIcon={app}\{#MyAppExeName}
RestartIfNeededByRun=no
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
#ifdef IS_NIGHTLY
[Messages]
SetupAppTitle=Setup (Nightly)
SetupWindowTitle=Setup - %1 (Nightly)
#endif
[Tasks]
Name: "vcredist"; Description: "Install the required Visual C++ 2015/2017/2019/2022 Redistributable";
; GroupDescription: "{cm:AdditionalIcons}";
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; Flags: unchecked
Name: "freshinstall"; Description: "Fresh install (delete old settings/logs)"; Flags: unchecked
[Files]
Source: "{#WORKING_DIR}Chatterino2\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#WORKING_DIR}vc_redist.x64.exe"; DestDir: "{tmp}"; Tasks: vcredist;
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
; VC++ redistributable
Filename: {tmp}\vc_redist.x64.exe; Parameters: "/install /passive /norestart"; StatusMsg: "Installing 64-bit Windows Universal Runtime..."; Flags: waituntilterminated; Tasks: vcredist
; Run chatterino
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
[InstallDelete]
; Delete cache on install
Type: filesandordirs; Name: "{userappdata}\Chatterino2\Cache"
; Delete %appdata%\Chatterino2 on freshinstall
Type: filesandordirs; Name: "{userappdata}\Chatterino2"; Tasks: freshinstall
[UninstallDelete]
; Delete cache on uninstall
Type: filesandordirs; Name: "{userappdata}\Chatterino2\Cache"

View file

@ -18,6 +18,7 @@ Checks: "-*,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-avoid-magic-numbers,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-readability-magic-numbers,
-performance-noexcept-move-constructor,
-misc-non-private-member-variables-in-classes,
@ -49,6 +50,8 @@ CheckOptions:
value: CamelCase
- key: readability-identifier-naming.GlobalConstantCase
value: UPPER_CASE
- key: readability-identifier-naming.GlobalVariableCase
value: UPPER_CASE
- key: readability-identifier-naming.VariableCase
value: camelBack
- key: readability-implicit-bool-conversion.AllowPointerConditions

14
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,14 @@
# If a commit modifies a ton of files and doesn't really contribute to the
# output of git-blame, please add it here
#
# Don't add commits from the same PR you are creating. We squash PRs into a
# single commit, so references to those commits will be lost
#
# 2018 - changed to 80 max column
f71ff08e686ae76c3dd4084d0f05f27ba9b3fdcb
#
# 2018 - added brace wrapping after if and for
e259b9e39f46f3cb0e4838c988d4f320a03dfaa4
#
# 2019 - Normalize line endings in already existing files
b06eb9df835c25154899fbcf43e9b37addcea1b1

View file

@ -7,6 +7,7 @@ on:
- master
pull_request:
workflow_dispatch:
merge_group:
concurrency:
group: build-${{ github.ref }}
@ -107,7 +108,7 @@ jobs:
- name: Install Qt5
if: startsWith(matrix.qt-version, '5.')
uses: jurplel/install-qt-action@v3.2.0
uses: jurplel/install-qt-action@v3.2.1
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
@ -115,7 +116,7 @@ jobs:
- name: Install Qt6
if: startsWith(matrix.qt-version, '6.')
uses: jurplel/install-qt-action@v3.2.0
uses: jurplel/install-qt-action@v3.2.1
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
@ -134,6 +135,18 @@ jobs:
"C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV"
shell: powershell
- name: Setup sccache (Windows)
# sccache v0.5.3
uses: nerixyz/ccache-action@9a7e8d00116ede600ee7717350c6594b8af6aaa5
if: startsWith(matrix.os, 'windows')
with:
variant: sccache
# only save on on the default (master) branch
save: ${{ github.event_name == 'push' }}
key: sccache-build-${{ matrix.os }}-${{ matrix.qt-version }}-${{ matrix.skip-crashpad }}
restore-keys: |
sccache-build-${{ matrix.os }}-${{ matrix.qt-version }}
- name: Cache conan packages (Windows)
if: startsWith(matrix.os, 'windows')
uses: actions/cache@v3
@ -171,13 +184,16 @@ jobs:
- name: Build (Windows)
if: startsWith(matrix.os, 'windows')
shell: pwsh
env:
# Enable PCH on Windows when crashpad is enabled
C2_WINDOWS_USE_PCH: ${{ matrix.skip-crashpad && 'OFF' || 'ON' }}
run: |
cd build
cmake `
-G"NMake Makefiles" `
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" `
-DUSE_PRECOMPILED_HEADERS=OFF `
-DUSE_PRECOMPILED_HEADERS=${{ env.C2_WINDOWS_USE_PCH }} `
-DBUILD_WITH_CRASHPAD="$Env:C2_ENABLE_CRASHPAD" `
-DCHATTERINO_LTO="$Env:C2_ENABLE_LTO" `
-DCHATTERINO_PLUGINS="$Env:C2_PLUGINS" `
@ -276,16 +292,34 @@ jobs:
- name: clang-tidy review
if: matrix.clang-tidy-review && github.event_name == 'pull_request'
uses: ZedThree/clang-tidy-review@v0.13.0
uses: ZedThree/clang-tidy-review@v0.13.2
with:
build_dir: build
build_dir: build-clang-tidy
config_file: ".clang-tidy"
split_workflow: true
exclude: "tests/*,lib/*"
exclude: "lib/*"
cmake_command: >-
cmake -S. -Bbuild-clang-tidy
-DCMAKE_BUILD_TYPE=Release
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On
-DUSE_PRECOMPILED_HEADERS=OFF
-DCMAKE_EXPORT_COMPILE_COMMANDS=On
-DCHATTERINO_LTO=Off
-DCHATTERINO_PLUGINS=On
-DBUILD_WITH_QT6=Off
-DBUILD_TESTS=On
-DBUILD_BENCHMARKS=On
apt_packages: >-
qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev,
libsecret-1-dev,
libboost-dev, libboost-system-dev, libboost-filesystem-dev,
libssl-dev,
rapidjson-dev,
libbenchmark-dev
- name: clang-tidy-review upload
if: matrix.clang-tidy-review && github.event_name == 'pull_request'
uses: ZedThree/clang-tidy-review/upload@v0.13.0
uses: ZedThree/clang-tidy-review/upload@v0.13.2
- name: Package - AppImage (Ubuntu)
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact

View file

@ -6,6 +6,7 @@ on:
branches:
- master
pull_request:
merge_group:
concurrency:
group: check-formatting-${{ github.ref }}

56
.github/workflows/create-installer.yml vendored Normal file
View file

@ -0,0 +1,56 @@
name: Create installer
on:
workflow_run:
workflows: ["Build"]
types: [completed]
# make sure this only runs on the default branch
branches: [master]
workflow_dispatch:
jobs:
create-installer:
runs-on: windows-latest
# Only run manually or when a build succeeds
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
strategy:
matrix:
qt-version: [5.15.2, 6.5.0]
env:
VARIANT_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '.EXPERIMENTAL-Qt6' || '' }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # allows for tags access
- name: Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: build.yml
name: chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
path: build/
- name: Unzip
run: 7z e -spf chatterino-windows-x86-64-Qt-${{ matrix.qt-version }}.zip
working-directory: build
- name: Install InnoSetup
run: choco install innosetup
- name: Add InnoSetup to path
run: echo "C:\Program Files (x86)\Inno Setup 6\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.12.1
- name: Build installer
id: build-installer
working-directory: build
run: ..\.CI\build-installer.ps1
shell: powershell
- name: Upload installer
uses: actions/upload-artifact@v3
with:
path: build/${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}${{ env.VARIANT_SUFFIX }}.exe
name: ${{ steps.build-installer.outputs.C2_INSTALLER_BASE_NAME }}${{ env.VARIANT_SUFFIX }}.exe

View file

@ -6,6 +6,7 @@ on:
branches:
- master
pull_request:
merge_group:
concurrency:
group: lint-${{ github.ref }}
@ -19,7 +20,7 @@ jobs:
- uses: actions/checkout@v3
- name: Check formatting with Prettier
uses: actionsx/prettier@e90ec5455552f0f640781bdd5f5d2415acb52f1a
uses: actionsx/prettier@3d9f7c3fa44c9cb819e68292a328d7f4384be206
with:
# prettier CLI arguments.
args: --write .

View file

@ -12,6 +12,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: ZedThree/clang-tidy-review/post@v0.13.0
- uses: ZedThree/clang-tidy-review/post@v0.13.2
with:
lgtm_comment_body: ""

View file

@ -4,9 +4,11 @@ name: Test
on:
pull_request:
workflow_dispatch:
merge_group:
env:
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.6
QT_QPA_PLATFORM: minimal
concurrency:
group: test-${{ github.ref }}
@ -17,29 +19,30 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04]
qt-version: [5.15.2]
include:
- os: "ubuntu-20.04"
qt-version: "5.15.2"
- os: "ubuntu-20.04"
qt-version: "5.12.12"
- os: "ubuntu-22.04"
qt-version: "6.2.4"
fail-fast: false
env:
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Cache Qt
id: cache-qt
uses: actions/cache@v3
with:
path: "${{ github.workspace }}/qt/"
key: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}
- name: Install Qt
uses: jurplel/install-qt-action@v3.2.0
uses: jurplel/install-qt-action@v3.2.1
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
modules: ${{ env.QT_MODULES }}
version: ${{ matrix.qt-version }}
dir: "${{ github.workspace }}/qt/"
# LINUX
- name: Install dependencies (Ubuntu)
@ -73,18 +76,23 @@ jobs:
- name: Build (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: |
cmake -DBUILD_TESTS=On -DBUILD_APP=OFF ..
cmake --build . --config Release
cmake \
-DBUILD_TESTS=On \
-DBUILD_APP=OFF \
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
..
cmake --build .
working-directory: build-test
shell: bash
- name: Test (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
timeout-minutes: 30
run: |
docker pull kennethreitz/httpbin
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
docker run --network=host --detach ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
docker run -p 9051:80 --detach kennethreitz/httpbin
./bin/chatterino-test --platform minimal || ./bin/chatterino-test --platform minimal || ./bin/chatterino-test --platform minimal
./bin/chatterino-test || ./bin/chatterino-test || ./bin/chatterino-test
working-directory: build-test
shell: bash

3
.gitignore vendored
View file

@ -121,3 +121,6 @@ resources/resources_autogenerated.qrc
# Leftovers from running `aqt install`
aqtinstall.log
# sccache (CI)
.sccache

View file

@ -2,6 +2,65 @@
## Unversioned
- Minor: Message input is now focused when clicking on emotes. (#4719)
- Minor: Changed viewer list to chatter list to more match Twitch's terminology. (#4732)
- Minor: Nicknames are now taken into consideration when searching for messages. (#4663, #4742)
- Minor: Add an icon showing when streamer mode is enabled (#4410, #4690)
- Minor: Added `/shoutout <username>` commands to shoutout specified user. (#4638)
- Minor: Improved editing hotkeys. (#4628)
- Minor: The input completion and quick switcher are now styled to match your theme. (#4671)
- Minor: Added setting to only show tabs with live channels (default toggle hotkey: Ctrl+Shift+L). (#4358)
- Minor: Added better support for Twitch's Hype Chat feature. (#4715)
- Minor: Added option to subscribe to and unsubscribe from reply threads. (#4680, #4739)
- Minor: Added a message for when Chatterino joins a channel (#4616)
- Minor: Add accelerators to the right click menu for messages (#4705)
- Minor: Add pin action to usercards and reply threads. (#4692)
- Minor: 7TV badges now automatically update upon changing. (#4512)
- Minor: Stream status requests are now batched. (#4713)
- Minor: Added `/c2-theme-autoreload` command to automatically reload a custom theme. This is useful for when you're developing your own theme. (#4718)
- Minor: Remove restriction on Go To Message on system messages from search. (#4614)
- Minor: Highlights loaded from message history will now correctly appear in the /mentions tab. (#4475)
- Minor: All channels opened in browser tabs are synced when using the extension for quicker switching between tabs. (#4741)
- Minor: Show channel point redemptions without messages in usercard. (#4557)
- Minor: Allow for customizing the behavior of `Right Click`ing of usernames. (#4622, #4751)
- Minor: Added support for opening incognito links in firefox-esr and chromium. (#4745)
- Minor: Added support for opening incognito links under Linux/BSD using XDG. (#4745)
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
- Bugfix: Fixed a crash when opening and closing a reply thread and switching the user. (#4675)
- Bugfix: Fixed a crash that could happen when closing splits before their display name was updated. This was especially noticeable after the live controller changes. (#4731)
- Bugfix: Fix visual glitches with smooth scrolling. (#4501)
- Bugfix: Fixed pings firing for the "Your username" highlight when not signed in. (#4698)
- Bugfix: Fixed partially broken filters on Qt 6 builds. (#4702)
- Bugfix: Fixed tooltips & popups sometimes showing up on the wrong monitor. (#4740)
- Bugfix: Fixed some network errors having `0` as their HTTP status. (#4704)
- Bugfix: Fixed crash that could occurr when closing the usercard too quickly after blocking or unblocking a user. (#4711)
- Bugfix: Fixed highlights sometimes not working after changing sound device, or switching users in your operating system. (#4729)
- Bugfix: Fixed key bindings not showing in context menus on Mac. (#4722)
- Bugfix: Fixed tab completion rarely completing the wrong word. (#4735)
- Bugfix: Fixed an issue where Subscriptions & Announcements that contained ignored phrases would still appear if the Block option was enabled. (#4748)
- Dev: Added command to set Qt's logging filter/rules at runtime (`/c2-set-logging-rules`). (#4637)
- Dev: Added the ability to see & load custom themes from the Themes directory. No stable promises are made of this feature, changes might be made that breaks custom themes without notice. (#4570)
- Dev: Added test cases for emote and tab completion. (#4644)
- Dev: Fixed `clang-tidy-review` action not picking up dependencies. (#4648)
- Dev: Expanded upon `$$$` test channels. (#4655)
- Dev: Added tools to help debug image GC. (#4578)
- Dev: Removed duplicate license when having plugins enabled. (#4665)
- Dev: Replace our QObjectRef class with Qt's QPointer class. (#4666)
- Dev: Fixed warnings about QWidgets already having a QLayout. (#4672)
- Dev: Fixed undefined behavior when loading non-existant credentials. (#4673)
- Dev: Added support for compiling with `sccache`. (#4678)
- Dev: Added `sccache` in Windows CI. (#4678)
- Dev: Moved preprocessor Git and date definitions to executables only. (#4681)
- Dev: Refactored tests to be able to use `ctest` and run in debug builds. (#4700)
- Dev: Added the ability to use an alternate linker using the `-DUSE_ALTERNATE_LINKER=...` CMake parameter. (#4711)
- Dev: The Windows installer is now built in CI. (#4408)
- Dev: Removed `getApp` and `getSettings` calls from message rendering. (#4535)
- Dev: Get the default browser executable instead of the entire command line when opening incognito links. (#4745)
## 2.4.4
- Minor: Added a Send button in the input box so you can click to send a message. This is disabled by default and can be enabled with the "Show send message button" setting. (#4607)
- Minor: Improved error messages when the updater fails a download. (#4594)
- Minor: Added `/shield` and `/shieldoff` commands to toggle shield mode. (#4580)
@ -9,8 +68,10 @@
- Bugfix: Fixed the menu warping on macOS on Qt6. (#4595)
- Bugfix: Fixed link tooltips not showing unless the thumbnail setting was enabled. (#4597)
- Bugfix: Domains starting with `http` are now parsed as links again. (#4598)
- Bugfix: Reduced the size of the update prompt to prevent it from going off the users screen. (#4626)
- Bugfix: Fixed click effects on buttons not being antialiased. (#4473)
- Bugfix: Fixed Ctrl+Backspace not working after Select All in chat search popup. (#4461)
- Bugfix: Fixed crash when scrolling up really fast. (#4621)
- Dev: Added the ability to control the `followRedirect` mode for requests. (#4594)
## 2.4.3

View file

@ -8,7 +8,7 @@ list(APPEND CMAKE_MODULE_PATH
"${CMAKE_SOURCE_DIR}/cmake/sanitizers-cmake/cmake"
)
project(chatterino VERSION 2.4.3)
project(chatterino VERSION 2.4.4)
option(BUILD_APP "Build Chatterino" ON)
option(BUILD_TESTS "Build the tests for Chatterino" OFF)
@ -42,11 +42,51 @@ else()
endif()
find_program(CCACHE_PROGRAM ccache)
if (CCACHE_PROGRAM)
set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
message("Using ${CCACHE_PROGRAM} for speeding up build")
find_program(SCCACHE_PROGRAM sccache)
if (SCCACHE_PROGRAM)
set(_compiler_launcher ${SCCACHE_PROGRAM})
elseif (CCACHE_PROGRAM)
set(_compiler_launcher ${CCACHE_PROGRAM})
endif ()
# Alternate linker code taken from heavyai/heavydb
# https://github.com/heavyai/heavydb/blob/0517d99b467806f6af7b4c969e351368a667497d/CMakeLists.txt#L87-L103
macro(set_alternate_linker linker)
find_program(LINKER_EXECUTABLE ld.${USE_ALTERNATE_LINKER} ${USE_ALTERNATE_LINKER})
if(LINKER_EXECUTABLE)
if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND "${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 12.0.0)
add_link_options("-ld-path=${USE_ALTERNATE_LINKER}")
else()
add_link_options("-fuse-ld=${USE_ALTERNATE_LINKER}")
endif()
else()
set(USE_ALTERNATE_LINKER "" CACHE STRING "Use alternate linker" FORCE)
endif()
endmacro()
set(USE_ALTERNATE_LINKER "" CACHE STRING "Use alternate linker. Leave empty for system default; alternatives are 'gold', 'lld', 'bfd', 'mold'")
if(NOT "${USE_ALTERNATE_LINKER}" STREQUAL "")
set_alternate_linker(${USE_ALTERNATE_LINKER})
endif()
if (_compiler_launcher)
set(CMAKE_CXX_COMPILER_LAUNCHER "${_compiler_launcher}" CACHE STRING "CXX compiler launcher")
message(STATUS "Using ${_compiler_launcher} for speeding up build")
if (MSVC)
# /Zi can't be used with (s)ccache
# Use /Z7 instead (debug info in object files)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
string(REPLACE "/Zi" "/Z7" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
endif()
endif()
endif()
include(${CMAKE_CURRENT_LIST_DIR}/cmake/GIT.cmake)
find_package(Qt${MAJOR_QT_VERSION} REQUIRED
@ -72,7 +112,7 @@ if (WIN32)
find_package(WinToast REQUIRED)
endif ()
find_package(Sanitizers)
find_package(Sanitizers QUIET)
# Find boost on the system
# `OPTIONAL_COMPONENTS random` is required for vcpkg builds to link.
@ -119,6 +159,7 @@ find_package(RapidJSON REQUIRED)
find_package(Websocketpp REQUIRED)
if (BUILD_TESTS)
include(GoogleTest)
# For MSVC: Prevent overriding the parent project's compiler/linker settings
# See https://github.com/google/googletest/blob/main/googletest/README.md#visual-studio-dynamic-vs-static-runtimes
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
@ -166,6 +207,16 @@ if (BUILD_WITH_CRASHPAD)
add_subdirectory("${CMAKE_SOURCE_DIR}/lib/crashpad" EXCLUDE_FROM_ALL)
endif()
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898.
# For CI runs, however, the date of build file generation should be consistent with the date of
# compilation so this approximation is "good enough" for our purpose.
if (DEFINED ENV{CHATTERINO_SKIP_DATE_GEN})
set(cmake_gen_date "1970-01-01")
else ()
string(TIMESTAMP cmake_gen_date "%Y-%m-%d")
endif ()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -174,6 +225,10 @@ include(cmake/resources/generate_resources.cmake)
add_subdirectory(src)
if (BUILD_TESTS OR BUILD_BENCHMARKS)
add_subdirectory(mocks)
endif ()
if (BUILD_TESTS)
enable_testing()
add_subdirectory(tests)

View file

@ -44,6 +44,18 @@ git submodule update --init --recursive
[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
## Git blame
This project has big commits in the history which for example update all line
endings. To improve the output of git-blame, consider setting:
```
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
This will ignore all revisions mentioned in the [`.git-blame-ignore-revs`
file](./.git-blame-ignore-revs). GitHub does this by default.
## Code style
The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format.

View file

@ -15,6 +15,7 @@ add_executable(${PROJECT_NAME} ${benchmark_SOURCES})
add_sanitizers(${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-lib)
target_link_libraries(${PROJECT_NAME} PRIVATE chatterino-mocks)
target_link_libraries(${PROJECT_NAME} PRIVATE benchmark::benchmark)

View file

@ -1,16 +1,18 @@
#include "Application.hpp"
#include "singletons/Settings.hpp"
#include "common/Channel.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "messages/Message.hpp"
#include "messages/SharedMessageBuilder.hpp"
#include "mocks/EmptyApplication.hpp"
#include "singletons/Settings.hpp"
#include "util/Helpers.hpp"
#include <benchmark/benchmark.h>
#include <QDebug>
#include <QString>
#include <QTemporaryDir>
using namespace chatterino;
@ -45,65 +47,17 @@ public:
}
};
class MockApplication : IApplication
class MockApplication : mock::EmptyApplication
{
public:
Theme *getThemes() override
{
return nullptr;
}
Fonts *getFonts() override
{
return nullptr;
}
IEmotes *getEmotes() override
{
return nullptr;
}
AccountController *getAccounts() override
{
return &this->accounts;
}
HotkeyController *getHotkeys() override
{
return nullptr;
}
WindowManager *getWindows() override
{
return nullptr;
}
Toasts *getToasts() override
{
return nullptr;
}
CommandController *getCommands() override
{
return nullptr;
}
NotificationController *getNotifications() override
{
return nullptr;
}
HighlightController *getHighlights() override
{
return &this->highlights;
}
TwitchIrcServer *getTwitch() override
{
return nullptr;
}
ChatterinoBadges *getChatterinoBadges() override
{
return nullptr;
}
FfzBadges *getFfzBadges() override
{
return nullptr;
}
IUserDataController *getUserData() override
{
return nullptr;
}
AccountController accounts;
HighlightController highlights;
@ -113,7 +67,8 @@ public:
static void BM_HighlightTest(benchmark::State &state)
{
MockApplication mockApplication;
Settings settings("/tmp/c2-mock");
QTemporaryDir settingsDir;
Settings settings(settingsDir.path());
std::string message =
R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))";

View file

@ -1,6 +1,11 @@
#include "singletons/Settings.hpp"
#include <benchmark/benchmark.h>
#include <QApplication>
#include <QtConcurrent>
#include <QTemporaryDir>
using namespace chatterino;
int main(int argc, char **argv)
{
@ -8,11 +13,26 @@ int main(int argc, char **argv)
::benchmark::Initialize(&argc, argv);
QtConcurrent::run([&app] {
// Ensure settings are initialized before any benchmarks are run
QTemporaryDir settingsDir;
settingsDir.setAutoRemove(false); // we'll remove it manually
chatterino::Settings settings(settingsDir.path());
QTimer::singleShot(0, [&]() {
::benchmark::RunSpecifiedBenchmarks();
app.exit(0);
settingsDir.remove();
// Pick up the last events from the eventloop
// Using a loop to catch events queueing other events (e.g. deletions)
for (size_t i = 0; i < 32; i++)
{
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
QApplication::exit(0);
});
return app.exec();
return QApplication::exec();
}

View file

@ -30,7 +30,7 @@
},
{
"title": "SVG Color",
"description": "This is stricter than Qt. You could theoretically put tabs an spaces between characters in a named color and capitalize the color.",
"description": "This enum is stricter than Qt. You could theoretically put tabs and spaces between characters in a named color and capitalize the color.",
"$comment": "https://www.w3.org/TR/SVG11/types.html#ColorKeywords",
"enum": [
"aliceblue",

View file

@ -5,6 +5,7 @@
This can only be "whole versions", so if you're releasing `2.4.0-beta` you'll need to condense it to `2.4.0`
- [ ] Updated version code in `resources/com.chatterino.chatterino.appdata.xml`
This cannot use dash to denote a pre-release identifier, you have to use a tilde instead.
- [ ] Updated version code in `.CI/chatterino-installer.iss`
- [ ] Update the changelog `## Unreleased` section to the new version `CHANGELOG.md`
Make sure to leave the `## Unreleased` line unchanged for easier merges
- [ ] Push directly to master :tf:

@ -1 +1 @@
Subproject commit ec992578688b4c51c1856d08731cf7dcf10e446a
Subproject commit 432ff49ecccc1cdebf1a7646007bb0594ac3481f

@ -1 +1 @@
Subproject commit 5d708c3f9cae12820e415d4f89c9eacbe2ab964b
Subproject commit ea39042e13645f63713425c05cc9ee4cfdcf0a40

55
mocks/.clang-format Normal file
View file

@ -0,0 +1,55 @@
Language: Cpp
AccessModifierOffset: -4
AlignEscapedNewlinesLeft: true
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: Empty
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakBeforeMultilineStrings: false
BasedOnStyle: Google
BraceWrapping:
AfterClass: "true"
AfterControlStatement: "true"
AfterFunction: "true"
AfterNamespace: "false"
BeforeCatch: "true"
BeforeElse: "true"
BreakBeforeBraces: Custom
BreakConstructorInitializersBeforeComma: true
ColumnLimit: 80
ConstructorInitializerAllOnOneLineOrOnePerLine: false
DerivePointerBinding: false
FixNamespaceComments: true
IndentCaseLabels: true
IndentWidth: 4
IndentWrappedFunctionNames: true
IndentPPDirectives: AfterHash
SortIncludes: CaseInsensitive
IncludeBlocks: Regroup
IncludeCategories:
# Project includes
- Regex: '^"[a-zA-Z\._-]+(/[a-zA-Z0-9\._-]+)*"$'
Priority: 1
# Third party library includes
- Regex: '<[[:alnum:].]+/[a-zA-Z0-9\._\/-]+>'
Priority: 3
# Qt includes
- Regex: '^<Q[a-zA-Z0-9\._\/-]+>$'
Priority: 3
CaseSensitive: true
# LibCommuni includes
- Regex: "^<Irc[a-zA-Z]+>$"
Priority: 3
# Misc libraries
- Regex: '^<[a-zA-Z_0-9]+\.h(pp)?>$'
Priority: 3
# Standard library includes
- Regex: "^<[a-zA-Z_]+>$"
Priority: 4
NamespaceIndentation: Inner
PointerBindsToType: false
SpacesBeforeTrailingComments: 2
Standard: Auto
ReflowComments: false

7
mocks/CMakeLists.txt Normal file
View file

@ -0,0 +1,7 @@
project(chatterino-mocks)
add_library(chatterino-mocks INTERFACE)
target_include_directories(chatterino-mocks INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(${PROJECT_NAME} INTERFACE gmock)

View file

@ -0,0 +1,86 @@
#pragma once
#include "Application.hpp"
namespace chatterino::mock {
class EmptyApplication : public IApplication
{
public:
Theme *getThemes() override
{
return nullptr;
}
Fonts *getFonts() override
{
return nullptr;
}
IEmotes *getEmotes() override
{
return nullptr;
}
AccountController *getAccounts() override
{
return nullptr;
}
HotkeyController *getHotkeys() override
{
return nullptr;
}
WindowManager *getWindows() override
{
return nullptr;
}
Toasts *getToasts() override
{
return nullptr;
}
CommandController *getCommands() override
{
return nullptr;
}
NotificationController *getNotifications() override
{
return nullptr;
}
HighlightController *getHighlights() override
{
return nullptr;
}
ITwitchIrcServer *getTwitch() override
{
return nullptr;
}
ChatterinoBadges *getChatterinoBadges() override
{
return nullptr;
}
FfzBadges *getFfzBadges() override
{
return nullptr;
}
IUserDataController *getUserData() override
{
return nullptr;
}
ITwitchLiveController *getTwitchLiveController() override
{
return nullptr;
}
};
} // namespace chatterino::mock

View file

@ -0,0 +1,414 @@
#pragma once
#include "providers/twitch/api/Helix.hpp"
#include "util/CancellationToken.hpp"
#include <gmock/gmock.h>
#include <QString>
#include <QStringList>
#include <vector>
namespace chatterino::mock {
class Helix : public IHelix
{
public:
virtual ~Helix() = default;
MOCK_METHOD(void, fetchUsers,
(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixUser>> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getUserByName,
(QString userName, ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getUserById,
(QString userId, ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, fetchUsersFollows,
(QString fromId, QString toId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getUserFollowers,
(QString userId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, fetchStreams,
(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback,
HelixFailureCallback failureCallback,
std::function<void()> finallyCallback),
(override));
MOCK_METHOD(void, getStreamById,
(QString userId,
(ResultCallback<bool, HelixStream> successCallback),
HelixFailureCallback failureCallback,
std::function<void()> finallyCallback),
(override));
MOCK_METHOD(void, getStreamByName,
(QString userName,
(ResultCallback<bool, HelixStream> successCallback),
HelixFailureCallback failureCallback,
std::function<void()> finallyCallback),
(override));
MOCK_METHOD(void, fetchGames,
(QStringList gameIds, QStringList gameNames,
(ResultCallback<std::vector<HelixGame>> successCallback),
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, searchGames,
(QString gameName,
ResultCallback<std::vector<HelixGame>> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getGameById,
(QString gameId, ResultCallback<HelixGame> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, createClip,
(QString channelId, ResultCallback<HelixClip> successCallback,
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback),
(override));
MOCK_METHOD(void, fetchChannels,
(QStringList userIDs,
ResultCallback<std::vector<HelixChannel>> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getChannel,
(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, createStreamMarker,
(QString broadcasterId, QString description,
ResultCallback<HelixStreamMarker> successCallback,
std::function<void(HelixStreamMarkerError)> failureCallback),
(override));
MOCK_METHOD(void, loadBlocks,
(QString userId,
ResultCallback<std::vector<HelixBlock>> successCallback,
FailureCallback<QString> failureCallback,
CancellationToken &&token),
(override));
MOCK_METHOD(void, blockUser,
(QString targetUserId, const QObject *caller,
std::function<void()> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, unblockUser,
(QString targetUserId, const QObject *caller,
std::function<void()> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, updateChannel,
(QString broadcasterId, QString gameId, QString language,
QString title,
std::function<void(NetworkResult)> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, manageAutoModMessages,
(QString userID, QString msgID, QString action,
std::function<void()> successCallback,
std::function<void(HelixAutoModMessageError)> failureCallback),
(override));
MOCK_METHOD(void, getCheermotes,
(QString broadcasterId,
ResultCallback<std::vector<HelixCheermoteSet>> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getEmoteSetData,
(QString emoteSetId,
ResultCallback<HelixEmoteSetData> successCallback,
HelixFailureCallback failureCallback),
(override));
MOCK_METHOD(void, getChannelEmotes,
(QString broadcasterId,
ResultCallback<std::vector<HelixChannelEmote>> successCallback,
HelixFailureCallback failureCallback),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, getGlobalBadges,
(ResultCallback<HelixGlobalBadges> successCallback,
(FailureCallback<HelixGetGlobalBadgesError, QString> failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, getChannelBadges,
(QString broadcasterID,
ResultCallback<HelixChannelBadges> successCallback,
(FailureCallback<HelixGetChannelBadgesError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateUserChatColor,
(QString userID, QString color,
ResultCallback<> successCallback,
(FailureCallback<HelixUpdateUserChatColorError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, deleteChatMessages,
(QString broadcasterID, QString moderatorID, QString messageID,
ResultCallback<> successCallback,
(FailureCallback<HelixDeleteChatMessagesError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, addChannelModerator,
(QString broadcasterID, QString userID,
ResultCallback<> successCallback,
(FailureCallback<HelixAddChannelModeratorError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, removeChannelModerator,
(QString broadcasterID, QString userID,
ResultCallback<> successCallback,
(FailureCallback<HelixRemoveChannelModeratorError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, sendChatAnnouncement,
(QString broadcasterID, QString moderatorID, QString message,
HelixAnnouncementColor color, ResultCallback<> successCallback,
(FailureCallback<HelixSendChatAnnouncementError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, addChannelVIP,
(QString broadcasterID, QString userID,
ResultCallback<> successCallback,
(FailureCallback<HelixAddChannelVIPError, QString> failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, removeChannelVIP,
(QString broadcasterID, QString userID,
ResultCallback<> successCallback,
(FailureCallback<HelixRemoveChannelVIPError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, unbanUser,
(QString broadcasterID, QString moderatorID, QString userID,
ResultCallback<> successCallback,
(FailureCallback<HelixUnbanUserError, QString> failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD( // /raid
void, startRaid,
(QString fromBroadcasterID, QString toBroadcasterId,
ResultCallback<> successCallback,
(FailureCallback<HelixStartRaidError, QString> failureCallback)),
(override)); // /raid
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD( // /unraid
void, cancelRaid,
(QString broadcasterID, ResultCallback<> successCallback,
(FailureCallback<HelixCancelRaidError, QString> failureCallback)),
(override)); // /unraid
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateEmoteMode,
(QString broadcasterID, QString moderatorID, bool emoteMode,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateFollowerMode,
(QString broadcasterID, QString moderatorID,
boost::optional<int> followerModeDuration,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateNonModeratorChatDelay,
(QString broadcasterID, QString moderatorID,
boost::optional<int> nonModeratorChatDelayDuration,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateSlowMode,
(QString broadcasterID, QString moderatorID,
boost::optional<int> slowModeWaitTime,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateSubscriberMode,
(QString broadcasterID, QString moderatorID,
bool subscriberMode,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateUniqueChatMode,
(QString broadcasterID, QString moderatorID,
bool uniqueChatMode,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
// update chat settings
// /timeout, /ban
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, banUser,
(QString broadcasterID, QString moderatorID, QString userID,
boost::optional<int> duration, QString reason,
ResultCallback<> successCallback,
(FailureCallback<HelixBanUserError, QString> failureCallback)),
(override)); // /timeout, /ban
// /w
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, sendWhisper,
(QString fromUserID, QString toUserID, QString message,
ResultCallback<> successCallback,
(FailureCallback<HelixWhisperError, QString> failureCallback)),
(override)); // /w
// getChatters
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, getChatters,
(QString broadcasterID, QString moderatorID, int maxChattersToFetch,
ResultCallback<HelixChatters> successCallback,
(FailureCallback<HelixGetChattersError, QString> failureCallback)),
(override)); // getChatters
// /vips
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, getChannelVIPs,
(QString broadcasterID,
ResultCallback<std::vector<HelixVip>> successCallback,
(FailureCallback<HelixListVIPsError, QString> failureCallback)),
(override)); // /vips
// /commercial
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, startCommercial,
(QString broadcasterID, int length,
ResultCallback<HelixStartCommercialResponse> successCallback,
(FailureCallback<HelixStartCommercialError, QString> failureCallback)),
(override)); // /commercial
// /mods
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(
void, getModerators,
(QString broadcasterID, int maxModeratorsToFetch,
ResultCallback<std::vector<HelixModerator>> successCallback,
(FailureCallback<HelixGetModeratorsError, QString> failureCallback)),
(override)); // /mods
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateShieldMode,
(QString broadcasterID, QString moderatorID, bool isActive,
ResultCallback<HelixShieldModeStatus> successCallback,
(FailureCallback<HelixUpdateShieldModeError, QString>
failureCallback)),
(override));
// /shoutout
MOCK_METHOD(
void, sendShoutout,
(QString fromBroadcasterID, QString toBroadcasterID,
QString moderatorID, ResultCallback<> successCallback,
(FailureCallback<HelixSendShoutoutError, QString> failureCallback)),
(override));
MOCK_METHOD(void, update, (QString clientId, QString oauthToken),
(override));
protected:
// The extra parenthesis around the failure callback is because its type
// contains a comma
MOCK_METHOD(void, updateChatSettings,
(QString broadcasterID, QString moderatorID, QJsonObject json,
ResultCallback<HelixChatSettings> successCallback,
(FailureCallback<HelixUpdateChatSettingsError, QString>
failureCallback)),
(override));
};
} // namespace chatterino::mock

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: https://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:krita="http://krita.org/namespaces/svg/krita"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="69.12pt"
height="69.12pt"
viewBox="0 0 69.12 69.12">
<defs/>
<path id="shape0" transform="matrix(2.87999988555909 0 0 2.87999988555909 5.76071977108956 13.5080491863677)" fill="#ff9c9c" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M3.98975 0.238705C4.13015 0.379277 4.20909 0.57003 4.20909 0.768705C4.20909 0.96738 4.13015 1.15813 3.98975 1.2987C2.93474 2.35368 2.17587 3.66806 1.78971 5.1092C1.40355 6.55034 1.40355 8.06807 1.78971 9.50921C2.17587 10.9503 2.93474 12.2647 3.98975 13.3197C4.12625 13.4612 4.20181 13.6508 4.20001 13.8474C4.19821 14.0439 4.11919 14.2321 3.98012 14.3711C3.84105 14.51 3.65278 14.5888 3.4562 14.5905C3.25963 14.5921 3.07009 14.5163 2.92875 14.3797C-0.97625 10.4747 -0.97625 4.1437 2.92875 0.238705C3.06932 0.0983082 3.26008 0.0193649 3.45875 0.0193649C3.65742 0.0193649 3.84818 0.0983082 3.98875 0.238705ZM17.0707 0.238705C20.9757 4.1437 20.9757 10.4747 17.0707 14.3807C16.9286 14.5131 16.7405 14.5853 16.5462 14.5819C16.352 14.5785 16.1665 14.4997 16.0291 14.3623C15.8918 14.225 15.813 14.0395 15.8096 13.8452C15.8061 13.651 15.8783 13.4628 16.0107 13.3207C17.066 12.2657 17.825 10.9512 18.2112 9.50994C18.5975 8.06865 18.5975 6.55076 18.2112 5.10947C17.825 3.66819 17.066 2.3537 16.0107 1.2987C15.8922 1.18828 15.8126 1.04239 15.7839 0.882977C15.7552 0.723568 15.7789 0.559065 15.8514 0.414228C15.9239 0.269391 16.0414 0.151871 16.1863 0.0793556C16.3311 0.00684041 16.4956 -0.0168395 16.655 0.01188C16.8144 0.0405996 16.9603 0.120202 17.0707 0.238705ZM6.81775 3.0667C6.95815 3.20728 7.03709 3.39803 7.03709 3.5967C7.03709 3.79538 6.95815 3.98613 6.81775 4.1267C6.25903 4.68524 5.85712 5.38119 5.65261 6.14428C5.44809 6.90737 5.44809 7.71104 5.65261 8.47413C5.85712 9.23722 6.25903 9.93317 6.81775 10.4917C6.95018 10.6338 7.02235 10.822 7.01893 11.0162C7.0155 11.2105 6.93674 11.396 6.79937 11.5333C6.66201 11.6707 6.47651 11.7495 6.28227 11.7529C6.08804 11.7563 5.89988 11.6841 5.75775 11.5517C4.63305 10.4269 4.00053 8.89982 4.00053 7.3092C4.00053 5.71859 4.63305 4.19148 5.75775 3.0667C5.89832 2.92631 6.08908 2.84736 6.28775 2.84736C6.48642 2.84736 6.67718 2.92631 6.81775 3.0667ZM14.2427 3.0667C15.3675 4.19148 16 5.71859 16 7.3092C16 8.89982 15.3675 10.4269 14.2427 11.5517C14.1316 11.6669 13.9863 11.7436 13.8284 11.7704C13.6705 11.7971 13.5082 11.7727 13.3652 11.7005C13.2222 11.6284 13.106 11.5123 13.0338 11.3694C12.9615 11.2265 12.9369 11.0641 12.9635 10.9062C12.9901 10.7483 13.0666 10.603 13.1817 10.4917C13.7403 9.93319 14.142 9.23735 14.3465 8.4744C14.5509 7.71145 14.5509 6.90796 14.3465 6.14501C14.142 5.38206 13.7403 4.68622 13.1818 4.1277C13.0493 3.98558 12.9771 3.79742 12.9806 3.60318C12.984 3.40895 13.0628 3.22345 13.2001 3.08608C13.3375 2.94872 13.523 2.86996 13.7172 2.86653C13.9115 2.8631 14.0996 2.93527 14.2417 3.0677ZM9.99975 5.8097C10.3974 5.8097 10.7792 5.96785 11.0604 6.24904C11.3416 6.53024 11.4997 6.91203 11.4997 7.3097C11.4997 7.70738 11.3416 8.08917 11.0604 8.37037C10.7792 8.65156 10.3974 8.8097 9.99975 8.8097C9.60208 8.8097 9.22029 8.65156 8.93909 8.37037C8.65789 8.08917 8.49975 7.70738 8.49975 7.3097C8.49975 6.91203 8.65789 6.53024 8.93909 6.24904C9.22029 5.96785 9.60208 5.8097 9.99975 5.8097Z" sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: https://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:krita="http://krita.org/namespaces/svg/krita"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="69.12pt"
height="69.12pt"
viewBox="0 0 69.12 69.12">
<defs/>
<path id="shape0" transform="matrix(2.87999977111818 0 0 2.87999977111818 5.76071954217913 13.508048613566)" fill="#c10000" stroke-opacity="0" stroke="#000000" stroke-width="0" stroke-linecap="square" stroke-linejoin="bevel" d="M3.98975 0.238705C4.13015 0.379277 4.20909 0.57003 4.20909 0.768705C4.20909 0.96738 4.13015 1.15813 3.98975 1.2987C2.93474 2.35368 2.17587 3.66806 1.78971 5.1092C1.40355 6.55034 1.40355 8.06807 1.78971 9.50921C2.17587 10.9503 2.93474 12.2647 3.98975 13.3197C4.12625 13.4612 4.20181 13.6508 4.20001 13.8474C4.19821 14.0439 4.11919 14.2321 3.98012 14.3711C3.84105 14.51 3.65278 14.5888 3.4562 14.5905C3.25963 14.5921 3.07009 14.5163 2.92875 14.3797C-0.97625 10.4747 -0.97625 4.1437 2.92875 0.238705C3.06932 0.0983082 3.26008 0.0193649 3.45875 0.0193649C3.65742 0.0193649 3.84818 0.0983082 3.98875 0.238705ZM17.0707 0.238705C20.9757 4.1437 20.9757 10.4747 17.0707 14.3807C16.9286 14.5131 16.7405 14.5853 16.5462 14.5819C16.352 14.5785 16.1665 14.4997 16.0291 14.3623C15.8918 14.225 15.813 14.0395 15.8096 13.8452C15.8061 13.651 15.8783 13.4628 16.0107 13.3207C17.066 12.2657 17.825 10.9512 18.2112 9.50994C18.5975 8.06865 18.5975 6.55076 18.2112 5.10947C17.825 3.66819 17.066 2.3537 16.0107 1.2987C15.8922 1.18828 15.8126 1.04239 15.7839 0.882977C15.7552 0.723568 15.7789 0.559065 15.8514 0.414228C15.9239 0.269391 16.0414 0.151871 16.1863 0.0793556C16.3311 0.00684042 16.4956 -0.0168395 16.655 0.01188C16.8144 0.0405996 16.9603 0.120202 17.0707 0.238705ZM6.81775 3.0667C6.95815 3.20728 7.03709 3.39803 7.03709 3.5967C7.03709 3.79538 6.95815 3.98613 6.81775 4.1267C6.25903 4.68524 5.85712 5.38119 5.65261 6.14428C5.44809 6.90737 5.44809 7.71104 5.65261 8.47413C5.85712 9.23722 6.25903 9.93317 6.81775 10.4917C6.95018 10.6338 7.02235 10.822 7.01893 11.0162C7.0155 11.2105 6.93674 11.396 6.79937 11.5333C6.66201 11.6707 6.47651 11.7495 6.28227 11.7529C6.08804 11.7563 5.89988 11.6841 5.75775 11.5517C4.63305 10.4269 4.00053 8.89982 4.00053 7.3092C4.00053 5.71859 4.63305 4.19148 5.75775 3.0667C5.89832 2.92631 6.08908 2.84736 6.28775 2.84736C6.48642 2.84736 6.67718 2.92631 6.81775 3.0667ZM14.2427 3.0667C15.3675 4.19148 16 5.71859 16 7.3092C16 8.89982 15.3675 10.4269 14.2427 11.5517C14.1316 11.6669 13.9863 11.7436 13.8284 11.7704C13.6705 11.7971 13.5082 11.7727 13.3652 11.7005C13.2222 11.6284 13.106 11.5123 13.0338 11.3694C12.9615 11.2265 12.9369 11.0641 12.9635 10.9062C12.9901 10.7483 13.0666 10.603 13.1817 10.4917C13.7403 9.93319 14.142 9.23735 14.3465 8.4744C14.5509 7.71145 14.5509 6.90796 14.3465 6.14501C14.142 5.38206 13.7403 4.68622 13.1818 4.1277C13.0493 3.98558 12.9771 3.79742 12.9806 3.60318C12.984 3.40895 13.0628 3.22345 13.2001 3.08608C13.3375 2.94872 13.523 2.86996 13.7172 2.86653C13.9115 2.8631 14.0996 2.93527 14.2417 3.0677ZM9.99975 5.8097C10.3974 5.8097 10.7792 5.96785 11.0604 6.24904C11.3416 6.53024 11.4997 6.91203 11.4997 7.3097C11.4997 7.70738 11.3416 8.08917 11.0604 8.37037C10.7792 8.65156 10.3974 8.8097 9.99975 8.8097C9.60208 8.8097 9.22029 8.65156 8.93909 8.37037C8.65789 8.08917 8.49975 7.70738 8.49975 7.3097C8.49975 6.91203 8.65789 6.53024 8.93909 6.24904C9.22029 5.96785 9.60208 5.8097 9.99975 5.8097Z" sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -32,6 +32,9 @@
<binary>chatterino</binary>
</provides>
<releases>
<release version="2.4.4" date="2023-05-14">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.4.4</url>
</release>
<release version="2.4.3" date="2023-04-30">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.4.3</url>
</release>

View file

@ -62,6 +62,9 @@ ScrubN | https://github.com/ScrubN | | Contributor
Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor
2547techno | https://github.com/2547techno | :/avatars/techno.png | Contributor
ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor
olafyang | https://github.com/olafyang | | Contributor
chrrs | https://github.com/chrrs | | Contributor
4rneee | https://github.com/4rneee | | Contributor
# If you are a contributor add yourself above this line

View file

@ -14,6 +14,7 @@
# include "controllers/plugins/PluginController.hpp"
#endif
#include "controllers/sound/SoundController.hpp"
#include "controllers/twitch/LiveController.hpp"
#include "controllers/userdata/UserDataController.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "messages/Message.hpp"
@ -88,6 +89,7 @@ Application::Application(Settings &_settings, Paths &_paths)
, seventvBadges(&this->emplace<SeventvBadges>())
, userData(&this->emplace<UserDataController>())
, sound(&this->emplace<SoundController>())
, twitchLiveController(&this->emplace<TwitchLiveController>())
#ifdef CHATTERINO_HAVE_PLUGINS
, plugins(&this->emplace<PluginController>())
#endif
@ -245,6 +247,16 @@ IUserDataController *Application::getUserData()
return this->userData;
}
ITwitchLiveController *Application::getTwitchLiveController()
{
return this->twitchLiveController;
}
ITwitchIrcServer *Application::getTwitch()
{
return this->twitch;
}
void Application::save()
{
for (auto &singleton : this->singletons_)
@ -258,7 +270,7 @@ void Application::initNm(Paths &paths)
(void)paths;
#ifdef Q_OS_WIN
# if defined QT_NO_DEBUG || defined C_DEBUG_NM
# if defined QT_NO_DEBUG || defined CHATTERINO_DEBUG_NM
registerNmHost(paths);
this->nmServer.start();
# endif

View file

@ -3,6 +3,8 @@
#include "common/Singleton.hpp"
#include "singletons/NativeMessaging.hpp"
#include <pajlada/signals.hpp>
#include <pajlada/signals/signal.hpp>
#include <QApplication>
#include <memory>
@ -10,6 +12,7 @@
namespace chatterino {
class TwitchIrcServer;
class ITwitchIrcServer;
class PubSub;
class CommandController;
@ -20,6 +23,8 @@ class HotkeyController;
class IUserDataController;
class UserDataController;
class SoundController;
class ITwitchLiveController;
class TwitchLiveController;
#ifdef CHATTERINO_HAVE_PLUGINS
class PluginController;
#endif
@ -55,10 +60,11 @@ public:
virtual CommandController *getCommands() = 0;
virtual HighlightController *getHighlights() = 0;
virtual NotificationController *getNotifications() = 0;
virtual TwitchIrcServer *getTwitch() = 0;
virtual ITwitchIrcServer *getTwitch() = 0;
virtual ChatterinoBadges *getChatterinoBadges() = 0;
virtual FfzBadges *getFfzBadges() = 0;
virtual IUserDataController *getUserData() = 0;
virtual ITwitchLiveController *getTwitchLiveController() = 0;
};
class Application : public IApplication
@ -98,6 +104,10 @@ public:
UserDataController *const userData{};
SoundController *const sound{};
private:
TwitchLiveController *const twitchLiveController{};
public:
#ifdef CHATTERINO_HAVE_PLUGINS
PluginController *const plugins{};
#endif
@ -141,10 +151,7 @@ public:
{
return this->highlights;
}
TwitchIrcServer *getTwitch() override
{
return this->twitch;
}
ITwitchIrcServer *getTwitch() override;
ChatterinoBadges *getChatterinoBadges() override
{
return this->chatterinoBadges;
@ -154,6 +161,9 @@ public:
return this->ffzBadges;
}
IUserDataController *getUserData() override;
ITwitchLiveController *getTwitchLiveController() override;
pajlada::Signals::NoArgSignal streamerModeChanged;
private:
void addSingleton(Singleton *singleton);

View file

@ -30,7 +30,7 @@ namespace {
#endif
}
void runLoop(NativeMessagingClient &client)
void runLoop()
{
auto received_message = std::make_shared<std::atomic_bool>(true);
@ -73,8 +73,9 @@ namespace {
received_message->store(true);
client.sendMessage(data);
nm::client::sendMessage(data);
}
_Exit(0);
}
} // namespace
@ -82,9 +83,7 @@ void runBrowserExtensionHost()
{
initFileMode();
NativeMessagingClient client;
runLoop(client);
runLoop();
}
} // namespace chatterino

View file

@ -1,7 +1,11 @@
set(LIBRARY_PROJECT "${PROJECT_NAME}-lib")
set(VERSION_PROJECT "${LIBRARY_PROJECT}-version")
set(EXECUTABLE_PROJECT "${PROJECT_NAME}")
add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00)
# registers the native messageing host
option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF)
set(SOURCE_FILES
Application.cpp
Application.hpp
@ -32,6 +36,7 @@ set(SOURCE_FILES
common/Env.hpp
common/LinkParser.cpp
common/LinkParser.hpp
common/Literals.hpp
common/Modes.cpp
common/Modes.hpp
common/NetworkCommon.cpp
@ -46,8 +51,6 @@ set(SOURCE_FILES
common/NetworkResult.hpp
common/QLogging.cpp
common/QLogging.hpp
common/Version.cpp
common/Version.hpp
common/WindowDescriptors.cpp
common/WindowDescriptors.hpp
@ -60,10 +63,14 @@ set(SOURCE_FILES
controllers/accounts/AccountModel.cpp
controllers/accounts/AccountModel.hpp
controllers/commands/builtin/chatterino/Debugging.cpp
controllers/commands/builtin/chatterino/Debugging.hpp
controllers/commands/builtin/twitch/ChatSettings.cpp
controllers/commands/builtin/twitch/ChatSettings.hpp
controllers/commands/builtin/twitch/ShieldMode.cpp
controllers/commands/builtin/twitch/ShieldMode.hpp
controllers/commands/builtin/twitch/Shoutout.cpp
controllers/commands/builtin/twitch/Shoutout.hpp
controllers/commands/CommandContext.hpp
controllers/commands/CommandController.cpp
controllers/commands/CommandController.hpp
@ -163,13 +170,16 @@ set(SOURCE_FILES
controllers/plugins/LuaUtilities.cpp
controllers/plugins/LuaUtilities.hpp
controllers/sound/SoundController.cpp
controllers/sound/SoundController.hpp
controllers/twitch/LiveController.cpp
controllers/twitch/LiveController.hpp
controllers/userdata/UserDataController.cpp
controllers/userdata/UserDataController.hpp
controllers/userdata/UserData.hpp
controllers/sound/SoundController.cpp
controllers/sound/SoundController.hpp
debug/Benchmark.cpp
debug/Benchmark.hpp
@ -199,6 +209,8 @@ set(SOURCE_FILES
messages/layouts/MessageLayout.hpp
messages/layouts/MessageLayoutContainer.cpp
messages/layouts/MessageLayoutContainer.hpp
messages/layouts/MessageLayoutContext.cpp
messages/layouts/MessageLayoutContext.hpp
messages/layouts/MessageLayoutElement.cpp
messages/layouts/MessageLayoutElement.hpp
messages/search/AuthorPredicate.cpp
@ -276,8 +288,11 @@ set(SOURCE_FILES
providers/liveupdates/BasicPubSubManager.hpp
providers/liveupdates/BasicPubSubWebsocket.hpp
providers/seventv/SeventvAPI.cpp
providers/seventv/SeventvAPI.hpp
providers/seventv/SeventvBadges.cpp
providers/seventv/SeventvBadges.hpp
providers/seventv/SeventvCosmetics.hpp
providers/seventv/SeventvEmotes.cpp
providers/seventv/SeventvEmotes.hpp
providers/seventv/SeventvEventAPI.cpp
@ -378,6 +393,7 @@ set(SOURCE_FILES
util/AttachToConsole.cpp
util/AttachToConsole.hpp
util/CancellationToken.hpp
util/Clipboard.cpp
util/Clipboard.hpp
util/ConcurrentMap.hpp
@ -397,6 +413,8 @@ set(SOURCE_FILES
util/IncognitoBrowser.hpp
util/InitUpdateButton.cpp
util/InitUpdateButton.hpp
util/IpcQueue.cpp
util/IpcQueue.hpp
util/LayoutHelper.cpp
util/LayoutHelper.hpp
util/NuulsUploader.cpp
@ -420,6 +438,12 @@ set(SOURCE_FILES
util/TypeName.hpp
util/WindowsHelper.cpp
util/WindowsHelper.hpp
util/XDGDesktopFile.cpp
util/XDGDesktopFile.hpp
util/XDGDirectory.cpp
util/XDGDirectory.hpp
util/XDGHelper.cpp
util/XDGHelper.hpp
util/serialize/Container.hpp
@ -706,7 +730,12 @@ if (BUILD_APP)
else()
add_executable(${EXECUTABLE_PROJECT} main.cpp)
endif()
add_sanitizers(${EXECUTABLE_PROJECT})
if(COMMAND add_sanitizers)
add_sanitizers(${EXECUTABLE_PROJECT})
else()
message(WARNING "Sanitizers support is disabled")
endif()
target_include_directories(${EXECUTABLE_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/)
@ -790,15 +819,26 @@ set_target_properties(${LIBRARY_PROJECT}
AUTOUIC ON
)
# Used to provide a date of build in the About page (for nightly builds). Getting the actual time of
# compilation in CMake is a more involved, as documented in https://stackoverflow.com/q/24292898.
# For CI runs, however, the date of build file generation should be consistent with the date of
# compilation so this approximation is "good enough" for our purpose.
if (DEFINED ENV{CHATTERINO_SKIP_DATE_GEN})
set(cmake_gen_date "1970-01-01")
else ()
string(TIMESTAMP cmake_gen_date "%Y-%m-%d")
endif ()
# The version project has definitions about the build.
# To avoid recompilations because of changing preprocessor definitions,
# this is its own project.
set(VERSION_SOURCE_FILES common/Version.cpp common/Version.hpp)
add_library(${VERSION_PROJECT} STATIC ${VERSION_SOURCE_FILES})
# source group for IDEs
source_group(TREE ${CMAKE_SOURCE_DIR} FILES ${VERSION_SOURCE_FILES})
target_include_directories(${VERSION_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(${VERSION_PROJECT} PRIVATE Qt${MAJOR_QT_VERSION}::Core)
target_compile_definitions(${VERSION_PROJECT} PRIVATE
CHATTERINO_GIT_HASH=\"${GIT_HASH}\"
CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\"
CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\"
CHATTERINO_GIT_MODIFIED=${GIT_MODIFIED}
CHATTERINO_CMAKE_GEN_DATE=\"${cmake_gen_date}\"
)
target_link_libraries(${LIBRARY_PROJECT} PRIVATE ${VERSION_PROJECT})
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CHATTERINO
@ -806,18 +846,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
AB_CUSTOM_SETTINGS
IRC_STATIC
IRC_NAMESPACE=Communi
CHATTERINO_GIT_HASH=\"${GIT_HASH}\"
CHATTERINO_GIT_RELEASE=\"${GIT_RELEASE}\"
CHATTERINO_GIT_COMMIT=\"${GIT_COMMIT}\"
CHATTERINO_CMAKE_GEN_DATE=\"${cmake_gen_date}\"
)
if (GIT_MODIFIED)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CHATTERINO_GIT_MODIFIED
)
endif ()
if (USE_SYSTEM_QTKEYCHAIN)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CMAKE_BUILD
@ -831,6 +860,9 @@ if (WIN32)
set_target_properties(${EXECUTABLE_PROJECT} PROPERTIES WIN32_EXECUTABLE TRUE)
endif ()
endif ()
if (CHATTERINO_DEBUG_NATIVE_MESSAGES)
target_compile_definitions(${LIBRARY_PROJECT} PRIVATE CHATTERINO_DEBUG_NM)
endif ()
if (MSVC)
target_compile_options(${LIBRARY_PROJECT} PUBLIC /EHsc /bigobj)

View file

@ -1,8 +1,10 @@
#ifdef __cplusplus
# include <boost/circular_buffer.hpp>
# include <boost/current_function.hpp>
# include <boost/foreach.hpp>
# include <boost/noncopyable.hpp>
# include <boost/optional.hpp>
# include <boost/signals2.hpp>
# include <IrcCommand>
# include <IrcConnection>
# include <IrcMessage>
@ -12,40 +14,29 @@
# include <pajlada/signals/connection.hpp>
# include <pajlada/signals/signal.hpp>
# include <QAbstractListModel>
# include <QAbstractNativeEventFilter>
# include <QAction>
# include <QApplication>
# include <QBrush>
# include <QBuffer>
# include <QButtonGroup>
# include <QByteArray>
# include <QCheckBox>
# include <QClipboard>
# include <QColor>
# include <QComboBox>
# include <QCompleter>
# include <QCoreApplication>
# include <QDateTime>
# include <QDebug>
# include <QDesktopServices>
# include <QDialog>
# include <QDialogButtonBox>
# include <QDir>
# include <QDockWidget>
# include <QDrag>
# include <QDragEnterEvent>
# include <QElapsedTimer>
# include <QEventLoop>
# include <QFile>
# include <QFileDialog>
# include <QFileInfo>
# include <QFlags>
# include <QFont>
# include <QFontDatabase>
# include <QFontDialog>
# include <QFontMetrics>
# include <QFormLayout>
# include <QGraphicsBlurEffect>
# include <QGroupBox>
# include <QHBoxLayout>
# include <QHeaderView>
@ -58,7 +49,6 @@
# include <QKeyEvent>
# include <QLabel>
# include <QLayout>
# include <QLibrary>
# include <QLineEdit>
# include <QList>
# include <QListView>
@ -92,31 +82,17 @@
# include <QSizePolicy>
# include <QSlider>
# include <QSpinBox>
# include <QStackedLayout>
# include <QStandardPaths>
# include <QString>
# include <QStyle>
# include <QStyleOption>
# include <QTabWidget>
# include <QtCore/QVariant>
# include <QTextEdit>
# include <QtGlobal>
# include <QThread>
# include <QThreadPool>
# include <QTime>
# include <QTimer>
# include <QtWidgets/QApplication>
# include <QtWidgets/QButtonGroup>
# include <QtWidgets/QDialog>
# include <QtWidgets/QDialogButtonBox>
# include <QtWidgets/QFormLayout>
# include <QtWidgets/QHBoxLayout>
# include <QtWidgets/QHeaderView>
# include <QtWidgets/QLabel>
# include <QtWidgets/QLineEdit>
# include <QtWidgets/QPushButton>
# include <QtWidgets/QTabWidget>
# include <QtWidgets/QVBoxLayout>
# include <QUrl>
# include <QUuid>
# include <QVariant>

View file

@ -86,6 +86,13 @@ namespace {
QApplication::setWindowIcon(QIcon(":/icon.ico"));
#endif
#ifdef Q_OS_MAC
// On the Mac/Cocoa platform this attribute is enabled by default
// We override it to ensure shortcuts show in context menus on that platform
QApplication::setAttribute(Qt::AA_DontShowShortcutsInContextMenus,
false);
#endif
installCustomPalette();
}

View file

@ -92,6 +92,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
return;
}
auto *app = getIApp();
// Twitch channel
auto *tc = dynamic_cast<TwitchChannel *>(&this->channel_);
@ -130,7 +131,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
}
};
if (auto account = getApp()->accounts->twitch.getCurrent())
if (auto account = app->getAccounts()->twitch.getCurrent())
{
// Twitch Emotes available globally
for (const auto &emote : account->accessEmotes()->emotes)
@ -153,18 +154,18 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
// 7TV Global
for (const auto &emote :
*getApp()->twitch->getSeventvEmotes().globalEmotes())
*app->getTwitch()->getSeventvEmotes().globalEmotes())
{
addString(emote.first.string, TaggedString::Type::SeventvGlobalEmote);
}
// Bttv Global
for (const auto &emote : *getApp()->twitch->getBttvEmotes().emotes())
for (const auto &emote : *app->getTwitch()->getBttvEmotes().emotes())
{
addString(emote.first.string, TaggedString::Type::BTTVChannelEmote);
}
// Ffz Global
for (const auto &emote : *getApp()->twitch->getFfzEmotes().emotes())
for (const auto &emote : *app->getTwitch()->getFfzEmotes().emotes())
{
addString(emote.first.string, TaggedString::Type::FFZChannelEmote);
}
@ -172,7 +173,8 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
// Emojis
if (prefix.startsWith(":"))
{
const auto &emojiShortCodes = getApp()->emotes->emojis.shortCodes;
const auto &emojiShortCodes =
app->getEmotes()->getEmojis()->getShortCodes();
for (const auto &m : emojiShortCodes)
{
addString(QString(":%1:").arg(m), TaggedString::Type::Emoji);
@ -231,20 +233,20 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
addString(emote.first.string, TaggedString::Type::BTTVGlobalEmote);
}
#ifdef CHATTERINO_HAVE_PLUGINS
for (const auto &command : getApp()->commands->pluginCommands())
for (const auto &command : app->getCommands()->pluginCommands())
{
addString(command, TaggedString::PluginCommand);
}
#endif
// Custom Chatterino commands
for (const auto &command : getApp()->commands->items)
for (const auto &command : app->getCommands()->items)
{
addString(command.name, TaggedString::CustomCommand);
}
// Default Chatterino commands
for (const auto &command :
getApp()->commands->getDefaultChatterinoCommandList())
app->getCommands()->getDefaultChatterinoCommandList())
{
addString(command, TaggedString::ChatterinoCommand);
}
@ -256,6 +258,19 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
}
}
std::vector<QString> CompletionModel::allItems() const
{
std::shared_lock lock(this->itemsMutex_);
std::vector<QString> results;
results.reserve(this->items_.size());
for (const auto &item : this->items_)
{
results.push_back(item.string);
}
return results;
}
bool CompletionModel::compareStrings(const QString &a, const QString &b)
{
// try comparing insensitively, if they are the same then senstively

View file

@ -6,6 +6,8 @@
#include <set>
#include <shared_mutex>
class InputCompletionTest;
namespace chatterino {
class Channel;
@ -60,10 +62,14 @@ public:
static bool compareStrings(const QString &a, const QString &b);
private:
std::vector<QString> allItems() const;
mutable std::shared_mutex itemsMutex_;
std::set<TaggedString> items_;
Channel &channel_;
friend class ::InputCompletionTest;
};
} // namespace chatterino

View file

@ -197,9 +197,9 @@ void Credentials::get(const QString &provider, const QString &name_,
}
else
{
auto &instance = insecureInstance();
const auto &instance = insecureInstance();
onLoaded(instance.object().find(name).value().toString());
onLoaded(instance[name].toString());
}
}

170
src/common/Literals.hpp Normal file
View file

@ -0,0 +1,170 @@
#pragma once
#include <QString>
/// This namespace defines the string suffixes _s, _ba, and _L1 used to create Qt types at compile-time.
/// They're easier to use comapred to their corresponding macros.
///
/// * u"foobar"_s creates a QString (like QStringLiteral). The u prefix is required.
///
/// * "foobar"_ba creates a QByteArray (like QByteArrayLiteral).
///
/// * "foobar"_L1 creates a QLatin1String(-View).
namespace chatterino::literals {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// This makes sure that the backing data never causes allocation after compilation.
// It's essentially the QStringLiteral macro inlined.
//
// From desktop-app/lib_base
// https://github.com/desktop-app/lib_base/blob/f904c60987115a4b514a575b23009ff25de0fafa/base/basic_types.h#L63-L152
// And qt/qtbase (5.15)
// https://github.com/qt/qtbase/blob/29400a683f96867133b28299c0d0bd6bcf40df35/src/corelib/text/qstringliteral.h#L64-L104
namespace detail {
// NOLINTBEGIN(modernize-avoid-c-arrays)
// NOLINTBEGIN(cppcoreguidelines-avoid-c-arrays)
template <size_t N>
struct LiteralResolver {
template <size_t... I>
constexpr LiteralResolver(const char16_t (&text)[N],
std::index_sequence<I...> /*seq*/)
: utf16Text{text[I]...}
{
}
template <size_t... I>
constexpr LiteralResolver(const char (&text)[N],
std::index_sequence<I...> /*seq*/)
: latin1Text{text[I]...}
, latin1(true)
{
}
constexpr LiteralResolver(const char16_t (&text)[N])
: LiteralResolver(text, std::make_index_sequence<N>{})
{
}
constexpr LiteralResolver(const char (&text)[N])
: LiteralResolver(text, std::make_index_sequence<N>{})
{
}
const char16_t utf16Text[N]{};
const char latin1Text[N]{};
size_t length = N;
bool latin1 = false;
};
template <size_t N>
struct StaticStringData {
template <std::size_t... I>
constexpr StaticStringData(const char16_t (&text)[N],
std::index_sequence<I...> /*seq*/)
: data Q_STATIC_STRING_DATA_HEADER_INITIALIZER(N - 1)
, text{text[I]...}
{
}
QArrayData data;
char16_t text[N];
QStringData *pointer()
{
Q_ASSERT(data.ref.isStatic());
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast)
return static_cast<QStringData *>(&data);
}
};
template <size_t N>
struct StaticByteArrayData {
template <std::size_t... I>
constexpr StaticByteArrayData(const char (&text)[N],
std::index_sequence<I...> /*seq*/)
: data Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER(N - 1)
, text{text[I]...}
{
}
QByteArrayData data;
char text[N];
QByteArrayData *pointer()
{
Q_ASSERT(data.ref.isStatic());
return &data;
}
};
// NOLINTEND(cppcoreguidelines-avoid-c-arrays)
// NOLINTEND(modernize-avoid-c-arrays)
} // namespace detail
template <detail::LiteralResolver R>
inline QString operator""_s() noexcept
{
static_assert(R.length > 0); // always has a terminating null
static_assert(!R.latin1, "QString literals must be made up of 16bit "
"characters. Forgot a u\"\"?");
static auto literal = detail::StaticStringData<R.length>(
R.utf16Text, std::make_index_sequence<R.length>{});
return QString{QStringDataPtr{literal.pointer()}};
};
template <detail::LiteralResolver R>
inline QByteArray operator""_ba() noexcept
{
static_assert(R.length > 0); // always has a terminating null
static_assert(R.latin1, "QByteArray literals must be made up of 8bit "
"characters. Misplaced u\"\"?");
static auto literal = detail::StaticByteArrayData<R.length>(
R.latin1Text, std::make_index_sequence<R.length>{});
return QByteArray{QByteArrayDataPtr{literal.pointer()}};
};
#elif QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
// The operators were added in 6.4, but their implementation works in any 6.x version.
//
// NOLINTBEGIN(cppcoreguidelines-pro-type-const-cast)
inline QString operator""_s(const char16_t *str, size_t size) noexcept
{
return QString(
QStringPrivate(nullptr, const_cast<char16_t *>(str), qsizetype(size)));
}
inline QByteArray operator""_ba(const char *str, size_t size) noexcept
{
return QByteArray(
QByteArrayData(nullptr, const_cast<char *>(str), qsizetype(size)));
}
// NOLINTEND(cppcoreguidelines-pro-type-const-cast)
#else
inline QString operator""_s(const char16_t *str, size_t size) noexcept
{
return Qt::Literals::StringLiterals::operator""_s(str, size);
}
inline QByteArray operator""_ba(const char *str, size_t size) noexcept
{
return Qt::Literals::StringLiterals::operator""_ba(str, size);
}
#endif
constexpr inline QLatin1String operator""_L1(const char *str,
size_t size) noexcept
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
using SizeType = int;
#else
using SizeType = qsizetype;
#endif
return QLatin1String{str, static_cast<SizeType>(size)};
}
} // namespace chatterino::literals

View file

@ -72,12 +72,12 @@ void writeToCache(const std::shared_ptr<NetworkData> &data,
}
}
void loadUncached(const std::shared_ptr<NetworkData> &data)
void loadUncached(std::shared_ptr<NetworkData> &&data)
{
DebugCount::increase("http request started");
NetworkRequester requester;
NetworkWorker *worker = new NetworkWorker;
auto *worker = new NetworkWorker;
worker->moveToThread(&NetworkManager::workerThread);
@ -89,7 +89,7 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
data->timer_->start(data->timeoutMS_);
}
auto reply = [&]() -> QNetworkReply * {
auto *reply = [&]() -> QNetworkReply * {
switch (data->requestType_)
{
case NetworkRequestType::Get:
@ -155,7 +155,8 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
{
postToThread([data] {
data->onError_(NetworkResult(
{}, NetworkResult::timedoutStatus));
NetworkResult::NetworkError::TimeoutError, {},
{}));
});
}
@ -174,7 +175,7 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
}
auto handleReply = [data, reply]() mutable {
if (data->hasCaller_ && !data->caller_.get())
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
@ -218,8 +219,9 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
QString(data->payload_));
}
// TODO: Should this always be run on the GUI thread?
postToThread([data, code = status.toInt(), reply] {
data->onError_(NetworkResult(reply->readAll(), code));
postToThread([data, status, reply] {
data->onError_(NetworkResult(reply->error(), status,
reply->readAll()));
});
}
@ -238,19 +240,23 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
auto status =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
NetworkResult result(bytes, status.toInt());
NetworkResult result(reply->error(), status, bytes);
DebugCount::increase("http request success");
// log("starting {}", data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([onSuccess = std::move(data->onSuccess_),
result = std::move(result)] {
onSuccess(result);
});
}
else
{
data->onSuccess_(result);
}
}
// log("finished {}", data->request_.url().toString());
@ -276,11 +282,15 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
if (data->finally_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([finally = std::move(data->finally_)] {
finally();
});
}
else
{
data->finally_();
}
}
};
@ -316,87 +326,88 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
}
// First tried to load cached, then uncached.
void loadCached(const std::shared_ptr<NetworkData> &data)
void loadCached(std::shared_ptr<NetworkData> &&data)
{
QFile cachedFile(getPaths()->cacheDirectory() + "/" + data->getHash());
if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly))
{
// File didn't exist OR File could not be opened
loadUncached(data);
loadUncached(std::move(data));
return;
}
else
{
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
NetworkResult result(bytes, 200);
qCDebug(chatterinoHTTP)
<< QString("%1 [CACHED] 200 %2")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString());
if (data->onSuccess_)
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
NetworkResult result(NetworkResult::NetworkError::NoError, QVariant(200),
bytes);
qCDebug(chatterinoHTTP)
<< QString("%1 [CACHED] 200 %2")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_ || isGuiThread())
{
if (data->executeConcurrently_ || isGuiThread())
// XXX: If outcome is Failure, we should invalidate the cache file
// somehow/somewhere
/*auto outcome =*/
if (data->hasCaller_ && data->caller_.isNull())
{
// XXX: If outcome is Failure, we should invalidate the cache file
// somehow/somewhere
/*auto outcome =*/
if (data->hasCaller_ && !data->caller_.get())
return;
}
data->onSuccess_(result);
}
else
{
postToThread([data, result]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->onSuccess_(result);
}
else
{
postToThread([data, result]() {
if (data->hasCaller_ && !data->caller_.get())
{
return;
}
data->onSuccess_(result);
});
}
});
}
}
if (data->finally_)
if (data->finally_)
{
if (data->executeConcurrently_ || isGuiThread())
{
if (data->executeConcurrently_ || isGuiThread())
if (data->hasCaller_ && data->caller_.isNull())
{
if (data->hasCaller_ && !data->caller_.get())
return;
}
data->finally_();
}
else
{
postToThread([data]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->finally_();
}
else
{
postToThread([data]() {
if (data->hasCaller_ && !data->caller_.get())
{
return;
}
data->finally_();
});
}
});
}
}
}
void load(const std::shared_ptr<NetworkData> &data)
void load(std::shared_ptr<NetworkData> &&data)
{
if (data->cache_)
{
QtConcurrent::run(loadCached, data);
QtConcurrent::run([data = std::move(data)]() mutable {
loadCached(std::move(data));
});
}
else
{
loadUncached(data);
loadUncached(std::move(data));
}
}

View file

@ -1,10 +1,10 @@
#pragma once
#include "common/NetworkCommon.hpp"
#include "util/QObjectRef.hpp"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include <QPointer>
#include <QTimer>
#include <functional>
@ -38,7 +38,7 @@ struct NetworkData {
QNetworkRequest request_;
bool hasCaller_{};
QObjectRef<QObject> caller_;
QPointer<QObject> caller_;
bool cache_{};
bool executeConcurrently_{};
@ -68,6 +68,6 @@ private:
QString hash_;
};
void load(const std::shared_ptr<NetworkData> &data);
void load(std::shared_ptr<NetworkData> &&data);
} // namespace chatterino

View file

@ -1,14 +1,8 @@
#include "common/NetworkRequest.hpp"
#include "common/NetworkPrivate.hpp"
#include "common/Outcome.hpp"
#include "common/QLogging.hpp"
#include "common/Version.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "singletons/Paths.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <QDebug>
#include <QFile>
@ -28,7 +22,7 @@ NetworkRequest::NetworkRequest(const std::string &url,
this->initializeDefaultValues();
}
NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType)
NetworkRequest::NetworkRequest(const QUrl &url, NetworkRequestType requestType)
: data(new NetworkData)
{
this->data->request_.setUrl(url);
@ -37,10 +31,7 @@ NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType)
this->initializeDefaultValues();
}
NetworkRequest::~NetworkRequest()
{
//assert(!this->data || this->executed_);
}
NetworkRequest::~NetworkRequest() = default;
NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) &&
{
@ -63,25 +54,25 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) &&
NetworkRequest NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) &&
{
this->data->onReplyCreated_ = cb;
this->data->onReplyCreated_ = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) &&
{
this->data->onError_ = cb;
this->data->onError_ = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) &&
{
this->data->onSuccess_ = cb;
this->data->onSuccess_ = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) &&
{
this->data->finally_ = cb;
this->data->finally_ = std::move(cb);
return std::move(*this);
}
@ -106,6 +97,13 @@ NetworkRequest NetworkRequest::header(const char *headerName,
return std::move(*this);
}
NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header,
const QVariant &value) &&
{
this->data->request_.setHeader(header, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::headerList(
const std::vector<std::pair<QByteArray, QByteArray>> &headers) &&
{
@ -129,20 +127,6 @@ NetworkRequest NetworkRequest::concurrent() &&
return std::move(*this);
}
NetworkRequest NetworkRequest::authorizeTwitchV5(const QString &clientID,
const QString &oauthToken) &&
{
// TODO: make two overloads, with and without oauth token
auto tmp = std::move(*this)
.header("Client-ID", clientID)
.header("Accept", "application/vnd.twitchtv.v5+json");
if (!oauthToken.isEmpty())
return std::move(tmp).header("Authorization", "OAuth " + oauthToken);
else
return tmp;
}
NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) &&
{
payload->setParent(this->data->lifetimeManager_);
@ -200,17 +184,36 @@ void NetworkRequest::execute()
void NetworkRequest::initializeDefaultValues()
{
const auto userAgent = QString("chatterino/%1 (%2)")
.arg(CHATTERINO_VERSION, CHATTERINO_GIT_HASH)
const auto userAgent = QStringLiteral("chatterino/%1 (%2)")
.arg(Version::instance().version(),
Version::instance().commitHash())
.toUtf8();
this->data->request_.setRawHeader("User-Agent", userAgent);
}
// Helper creator functions
NetworkRequest NetworkRequest::twitchRequest(QUrl url)
NetworkRequest NetworkRequest::json(const QJsonArray &root) &&
{
return NetworkRequest(url).authorizeTwitchV5(getDefaultClientID());
return std::move(*this).json(QJsonDocument(root));
}
NetworkRequest NetworkRequest::json(const QJsonObject &root) &&
{
return std::move(*this).json(QJsonDocument(root));
}
NetworkRequest NetworkRequest::json(const QJsonDocument &document) &&
{
return std::move(*this).json(document.toJson(QJsonDocument::Compact));
}
NetworkRequest NetworkRequest::json(const QByteArray &payload) &&
{
return std::move(*this)
.payload(payload)
.header(QNetworkRequest::ContentTypeHeader, "application/json")
.header(QNetworkRequest::ContentLengthHeader, payload.length())
.header("Accept", "application/json");
}
} // namespace chatterino

View file

@ -6,6 +6,10 @@
#include <memory>
class QJsonArray;
class QJsonObject;
class QJsonDocument;
namespace chatterino {
struct NetworkData;
@ -24,8 +28,8 @@ public:
explicit NetworkRequest(
const std::string &url,
NetworkRequestType requestType = NetworkRequestType::Get);
explicit NetworkRequest(
QUrl url, NetworkRequestType requestType = NetworkRequestType::Get);
explicit NetworkRequest(const QUrl &url, NetworkRequestType requestType =
NetworkRequestType::Get);
// Enable move
NetworkRequest(NetworkRequest &&other) = default;
@ -54,23 +58,25 @@ public:
NetworkRequest header(const char *headerName, const char *value) &&;
NetworkRequest header(const char *headerName, const QByteArray &value) &&;
NetworkRequest header(const char *headerName, const QString &value) &&;
NetworkRequest header(QNetworkRequest::KnownHeaders header,
const QVariant &value) &&;
NetworkRequest headerList(
const std::vector<std::pair<QByteArray, QByteArray>> &headers) &&;
NetworkRequest timeout(int ms) &&;
NetworkRequest concurrent() &&;
NetworkRequest authorizeTwitchV5(const QString &clientID,
const QString &oauthToken = QString()) &&;
NetworkRequest multiPart(QHttpMultiPart *payload) &&;
/**
* This will change `RedirectPolicyAttribute`.
* `QNetworkRequest`'s defaults are used by default (Qt 5: no-follow, Qt 6: follow).
*/
NetworkRequest followRedirects(bool on) &&;
NetworkRequest json(const QJsonObject &root) &&;
NetworkRequest json(const QJsonArray &root) &&;
NetworkRequest json(const QJsonDocument &document) &&;
NetworkRequest json(const QByteArray &payload) &&;
void execute();
static NetworkRequest twitchRequest(QUrl url);
private:
void initializeDefaultValues();
};

View file

@ -3,15 +3,21 @@
#include "common/QLogging.hpp"
#include <QJsonDocument>
#include <QMetaEnum>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
namespace chatterino {
NetworkResult::NetworkResult(const QByteArray &data, int status)
: data_(data)
, status_(status)
NetworkResult::NetworkResult(NetworkError error, const QVariant &httpStatusCode,
QByteArray data)
: data_(std::move(data))
, error_(error)
{
if (httpStatusCode.isValid())
{
this->status_ = httpStatusCode.toInt();
}
}
QJsonObject NetworkResult::parseJson() const
@ -59,9 +65,21 @@ const QByteArray &NetworkResult::getData() const
return this->data_;
}
int NetworkResult::status() const
QString NetworkResult::formatError() const
{
return this->status_;
if (this->status_)
{
return QString::number(*this->status_);
}
const auto *name =
QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(
this->error_);
if (name == nullptr)
{
return QStringLiteral("unknown error (%1)").arg(this->error_);
}
return name;
}
} // namespace chatterino

View file

@ -2,14 +2,20 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QNetworkReply>
#include <rapidjson/document.h>
#include <optional>
namespace chatterino {
class NetworkResult
{
public:
NetworkResult(const QByteArray &data, int status);
using NetworkError = QNetworkReply::NetworkError;
NetworkResult(NetworkError error, const QVariant &httpStatusCode,
QByteArray data);
/// Parses the result as json and returns the root as an object.
/// Returns empty object if parsing failed.
@ -20,13 +26,29 @@ public:
/// Parses the result as json and returns the document.
rapidjson::Document parseRapidJson() const;
const QByteArray &getData() const;
int status() const;
static constexpr int timedoutStatus = -2;
/// The error code of the reply.
/// In case of a successful reply, this will be NoError (0)
NetworkError error() const
{
return this->error_;
}
/// The HTTP status code if a response was received.
std::optional<int> status() const
{
return this->status_;
}
/// Formats the error.
/// If a reply is received, returns the HTTP status otherwise, the network error.
QString formatError() const;
private:
QByteArray data_;
int status_;
NetworkError error_;
std::optional<int> status_;
};
} // namespace chatterino

View file

@ -48,8 +48,11 @@ Q_LOGGING_CATEGORY(chatterinoStreamlink, "chatterino.streamlink", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTheme, "chatterino.theme", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTokenizer, "chatterino.tokenizer", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitch, "chatterino.twitch", logThreshold);
Q_LOGGING_CATEGORY(chatterinoTwitchLiveController,
"chatterino.twitch.livecontroller", logThreshold);
Q_LOGGING_CATEGORY(chatterinoUpdate, "chatterino.update", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWindowmanager, "chatterino.windowmanager",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoXDG, "chatterino.xdg", logThreshold);

View file

@ -37,7 +37,9 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoStreamlink);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTheme);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTokenizer);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitch);
Q_DECLARE_LOGGING_CATEGORY(chatterinoTwitchLiveController);
Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWindowmanager);
Q_DECLARE_LOGGING_CATEGORY(chatterinoXDG);

View file

@ -4,27 +4,14 @@
#include <QFileInfo>
#define UGLYMACROHACK1(s) #s
#define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s)
namespace chatterino {
Version::Version()
: version_(CHATTERINO_VERSION)
, commitHash_(QStringLiteral(CHATTERINO_GIT_HASH))
, isModified_(CHATTERINO_GIT_MODIFIED == 1)
, dateOfBuild_(QStringLiteral(CHATTERINO_CMAKE_GEN_DATE))
{
this->version_ = CHATTERINO_VERSION;
this->commitHash_ =
QString(FROM_EXTERNAL_DEFINE(CHATTERINO_GIT_HASH)).remove('"');
#ifdef CHATTERINO_GIT_MODIFIED
this->isModified_ = true;
#endif
#ifdef CHATTERINO_CMAKE_GEN_DATE
this->dateOfBuild_ =
QString(FROM_EXTERNAL_DEFINE(CHATTERINO_CMAKE_GEN_DATE)).remove('"');
#endif
this->fullVersion_ = "Chatterino ";
if (Modes::instance().isNightly)
{

View file

@ -24,7 +24,7 @@
* - 2.4.0-alpha.2
* - 2.4.0-alpha
**/
#define CHATTERINO_VERSION "2.4.3"
#define CHATTERINO_VERSION "2.4.4"
#if defined(Q_OS_WIN)
# define CHATTERINO_OS "win"

View file

@ -7,13 +7,16 @@
#include "common/QLogging.hpp"
#include "common/SignalVector.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/builtin/chatterino/Debugging.hpp"
#include "controllers/commands/builtin/twitch/ChatSettings.hpp"
#include "controllers/commands/builtin/twitch/ShieldMode.hpp"
#include "controllers/commands/builtin/twitch/Shoutout.hpp"
#include "controllers/commands/Command.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "controllers/commands/CommandModel.hpp"
#include "controllers/plugins/PluginController.hpp"
#include "controllers/userdata/UserDataController.hpp"
#include "messages/Image.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
@ -36,6 +39,7 @@
#include "util/FormatTime.hpp"
#include "util/Helpers.hpp"
#include "util/IncognitoBrowser.hpp"
#include "util/PostToThread.hpp"
#include "util/Qt.hpp"
#include "util/StreamerMode.hpp"
#include "util/StreamLink.hpp"
@ -646,7 +650,7 @@ void CommandController::initialize(Settings &, Paths &paths)
target,
[currentUser, channel, target](const HelixUser &targetUser) {
getApp()->accounts->twitch.getCurrent()->blockUser(
targetUser.id,
targetUser.id, nullptr,
[channel, target, targetUser] {
channel->addMessage(makeSystemMessage(
QString("You successfully blocked user %1")
@ -699,7 +703,7 @@ void CommandController::initialize(Settings &, Paths &paths)
target,
[currentUser, channel, target](const auto &targetUser) {
getApp()->accounts->twitch.getCurrent()->unblockUser(
targetUser.id,
targetUser.id, nullptr,
[channel, target, targetUser] {
channel->addMessage(makeSystemMessage(
QString("You successfully unblocked user %1")
@ -919,7 +923,8 @@ void CommandController::initialize(Settings &, Paths &paths)
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())),
currentSplit);
userPopup->setData(userName, channel);
userPopup->move(QCursor::pos());
userPopup->moveTo(QCursor::pos(), false,
BaseWindow::BoundsChecker::CursorPosition);
userPopup->show();
return "";
});
@ -3209,8 +3214,35 @@ void CommandController::initialize(Settings &, Paths &paths)
return "";
});
this->registerCommand(
"/debug-force-image-gc",
[](const QStringList & /*words*/, auto /*channel*/) -> QString {
runInGuiThread([] {
using namespace chatterino::detail;
auto &iep = ImageExpirationPool::instance();
iep.freeOld();
});
return "";
});
this->registerCommand(
"/debug-force-image-unload",
[](const QStringList & /*words*/, auto /*channel*/) -> QString {
runInGuiThread([] {
using namespace chatterino::detail;
auto &iep = ImageExpirationPool::instance();
iep.freeAll();
});
return "";
});
this->registerCommand("/shield", &commands::shieldModeOn);
this->registerCommand("/shieldoff", &commands::shieldModeOff);
this->registerCommand("/shoutout", &commands::sendShoutout);
this->registerCommand("/c2-set-logging-rules", &commands::setLoggingRules);
this->registerCommand("/c2-theme-autoreload", &commands::toggleThemeReload);
}
void CommandController::save()

View file

@ -0,0 +1,66 @@
#include "controllers/commands/builtin/chatterino/Debugging.hpp"
#include "common/Channel.hpp"
#include "common/Literals.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/MessageBuilder.hpp"
#include "singletons/Theme.hpp"
#include <QLoggingCategory>
#include <QString>
namespace chatterino::commands {
using namespace literals;
QString setLoggingRules(const CommandContext &ctx)
{
if (ctx.words.size() < 2)
{
ctx.channel->addMessage(makeSystemMessage(
"Usage: /c2-set-logging-rules <rules...>. To enable debug logging "
"for all categories from chatterino, use "
"'chatterino.*.debug=true'. For the format on the rules, see "
"https://doc.qt.io/qt-6/"
"qloggingcategory.html#configuring-categories"));
return {};
}
auto filterRules = ctx.words.mid(1).join('\n');
QLoggingCategory::setFilterRules(filterRules);
auto message =
QStringLiteral("Updated filter rules to '%1'.").arg(filterRules);
if (!qgetenv("QT_LOGGING_RULES").isEmpty())
{
message += QStringLiteral(
" Warning: Logging rules were previously set by the "
"QT_LOGGING_RULES environment variable. This might cause "
"interference - see: "
"https://doc.qt.io/qt-6/qloggingcategory.html#setFilterRules");
}
ctx.channel->addMessage(makeSystemMessage(message));
return {};
}
QString toggleThemeReload(const CommandContext &ctx)
{
if (getTheme()->isAutoReloading())
{
getTheme()->setAutoReload(false);
ctx.channel->addMessage(
makeSystemMessage(u"Disabled theme auto reloading."_s));
return {};
}
getTheme()->setAutoReload(true);
ctx.channel->addMessage(
makeSystemMessage(u"Auto reloading theme every %1 ms."_s.arg(
Theme::AUTO_RELOAD_INTERVAL_MS)));
return {};
}
} // namespace chatterino::commands

View file

@ -0,0 +1,17 @@
#pragma once
class QString;
namespace chatterino {
struct CommandContext;
} // namespace chatterino
namespace chatterino::commands {
QString setLoggingRules(const CommandContext &ctx);
QString toggleThemeReload(const CommandContext &ctx);
} // namespace chatterino::commands

View file

@ -0,0 +1,112 @@
#include "controllers/commands/builtin/twitch/Shoutout.hpp"
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino::commands {
QString sendShoutout(const CommandContext &ctx)
{
auto *twitchChannel = ctx.twitchChannel;
auto channel = ctx.channel;
auto words = &ctx.words;
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /shoutout command only works in Twitch channels"));
return "";
}
auto currentUser = getApp()->accounts->twitch.getCurrent();
if (currentUser->isAnon())
{
channel->addMessage(
makeSystemMessage("You must be logged in to send shoutout"));
return "";
}
if (words->size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage: \"/shoutout <username>\" - Sends a "
"shoutout to the specified twitch user"));
return "";
}
const auto target = words->at(1);
using Error = HelixSendShoutoutError;
getHelix()->getUserByName(
target,
[twitchChannel, channel, currentUser, &target](const auto targetUser) {
getHelix()->sendShoutout(
twitchChannel->roomId(), targetUser.id,
currentUser->getUserId(),
[channel, targetUser]() {
channel->addMessage(makeSystemMessage(
QString("Sent shoutout to %1").arg(targetUser.login)));
},
[channel](auto error, auto message) {
QString errorMessage = "Failed to send shoutout - ";
switch (error)
{
case Error::UserNotAuthorized: {
errorMessage += "You don't have permission to "
"perform that action.";
}
break;
case Error::UserMissingScope: {
errorMessage += "Missing required scope. "
"Re-login with your "
"account and try again.";
}
break;
case Error::Ratelimited: {
errorMessage +=
"You are being ratelimited by Twitch. "
"Try again in a few seconds.";
}
break;
case Error::UserIsBroadcaster: {
errorMessage += "The broadcaster may not give "
"themselves a Shoutout.";
}
break;
case Error::BroadcasterNotLive: {
errorMessage +=
"The broadcaster is not streaming live or "
"does not have one or more viewers.";
}
break;
case Error::Unknown: {
errorMessage += message;
}
break;
}
channel->addMessage(makeSystemMessage(errorMessage));
});
},
[channel, target] {
// Equivalent error from IRC
channel->addMessage(
makeSystemMessage(QString("Invalid username: %1").arg(target)));
});
return "";
}
} // namespace chatterino::commands

View file

@ -0,0 +1,15 @@
#pragma once
#include <QString>
namespace chatterino {
struct CommandContext;
} // namespace chatterino
namespace chatterino::commands {
QString sendShoutout(const CommandContext &ctx);
} // namespace chatterino::commands

View file

@ -2,6 +2,43 @@
#include <QRegularExpression>
namespace {
/// Loosely compares `lhs` with `rhs`.
/// This attempts to convert both variants to a common type if they're not equal.
bool looselyCompareVariants(QVariant &lhs, QVariant &rhs)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Qt 6 and later don't convert types as much as Qt 5 did when comparing.
//
// Based on QVariant::cmp from Qt 5.15
// https://github.com/qt/qtbase/blob/29400a683f96867133b28299c0d0bd6bcf40df35/src/corelib/kernel/qvariant.cpp#L4039-L4071
if (lhs.metaType() != rhs.metaType())
{
if (rhs.canConvert(lhs.metaType()))
{
if (!rhs.convert(lhs.metaType()))
{
return false;
}
}
else
{
// try the opposite conversion, it might work
qSwap(lhs, rhs);
if (!rhs.convert(lhs.metaType()))
{
return false;
}
}
}
#endif
return lhs == rhs;
}
} // namespace
namespace chatterino::filters {
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
@ -60,14 +97,14 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return left.toString().compare(right.toString(),
Qt::CaseInsensitive) == 0;
}
return left == right;
return looselyCompareVariants(left, right);
case NEQ:
if (variantTypesMatch(left, right, QMetaType::QString))
{
return left.toString().compare(right.toString(),
Qt::CaseInsensitive) != 0;
}
return left != right;
return !looselyCompareVariants(left, right);
case LT:
if (convertVariantTypes(left, right, QMetaType::Int))
return left.toInt() < right.toInt();
@ -92,13 +129,13 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
Qt::CaseInsensitive);
}
if (variantIs(left.type(), QMetaType::QVariantMap) &&
if (variantIs(left, QMetaType::QVariantMap) &&
right.canConvert(QMetaType::QString))
{
return left.toMap().contains(right.toString());
}
if (variantIs(left.type(), QMetaType::QVariantList))
if (variantIs(left, QMetaType::QVariantList))
{
return left.toList().contains(right);
}
@ -112,7 +149,7 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return false;
case STARTS_WITH:
if (variantIs(left.type(), QMetaType::QStringList) &&
if (variantIs(left, QMetaType::QStringList) &&
right.canConvert(QMetaType::QString))
{
auto list = left.toStringList();
@ -121,7 +158,7 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
Qt::CaseInsensitive) == 0;
}
if (variantIs(left.type(), QMetaType::QVariantList))
if (variantIs(left, QMetaType::QVariantList))
{
return left.toList().startsWith(right);
}
@ -136,7 +173,7 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return false;
case ENDS_WITH:
if (variantIs(left.type(), QMetaType::QStringList) &&
if (variantIs(left, QMetaType::QStringList) &&
right.canConvert(QMetaType::QString))
{
auto list = left.toStringList();
@ -145,7 +182,7 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
Qt::CaseInsensitive) == 0;
}
if (variantIs(left.type(), QMetaType::QVariantList))
if (variantIs(left, QMetaType::QVariantList))
{
return left.toList().endsWith(right);
}

View file

@ -12,7 +12,7 @@ QVariant ListExpression::execute(const ContextMap &context) const
for (const auto &exp : this->list_)
{
auto res = exp->execute(context);
if (allStrings && variantIsNot(res.type(), QMetaType::QString))
if (allStrings && variantIsNot(res, QMetaType::QString))
{
allStrings = false;
}

View file

@ -163,7 +163,7 @@ void rebuildReplyThreadHighlight(Settings &settings,
const auto & /*senderName*/, const auto & /*originalMessage*/,
const auto &flags,
const auto self) -> boost::optional<HighlightResult> {
if (flags.has(MessageFlag::ParticipatedThread) && !self)
if (flags.has(MessageFlag::SubscribedThread) && !self)
{
return HighlightResult{
highlightAlert,
@ -186,7 +186,8 @@ void rebuildMessageHighlights(Settings &settings,
auto currentUser = getIApp()->getAccounts()->twitch.getCurrent();
QString currentUsername = currentUser->getUserName();
if (settings.enableSelfHighlight && !currentUsername.isEmpty())
if (settings.enableSelfHighlight && !currentUsername.isEmpty() &&
!currentUser->isAnon())
{
HighlightPhrase highlight(
currentUsername, settings.showSelfHighlightInMentions,

View file

@ -210,7 +210,7 @@ void HighlightModel::afterInit()
std::vector<QStandardItem *> threadMessageRow = this->createRow();
setBoolItem(threadMessageRow[Column::Pattern],
getSettings()->enableThreadHighlight.getValue(), true, false);
threadMessageRow[Column::Pattern]->setData("Participated Reply Threads",
threadMessageRow[Column::Pattern]->setData("Subscribed Reply Threads",
Qt::DisplayRole);
setBoolItem(threadMessageRow[Column::ShowInMentions],
getSettings()->showThreadHighlightInMentions.getValue(), true,

View file

@ -5,6 +5,20 @@
#include <QString>
#include <map>
#include <vector>
inline const std::vector<std::pair<QString, std::vector<QString>>>
HOTKEY_ARG_ON_OFF_TOGGLE = {
{"Toggle", {}},
{"Set to on", {"on"}},
{"Set to off", {"off"}},
};
inline const std::vector<std::pair<QString, std::vector<QString>>>
HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION = {
{"No", {"withoutSelection"}},
{"Yes", {"withSelection"}},
};
namespace chatterino {
@ -13,6 +27,9 @@ struct ActionDefinition {
// displayName is the value that would be shown to a user when they edit or create a hotkey for an action
QString displayName;
// argumentDescription is a description of the arguments in a format of
// "<required arg: description of possible values> [optional arg: possible
// values]"
QString argumentDescription = "";
// minCountArguments is the minimum amount of arguments the action accepts
@ -21,6 +38,20 @@ struct ActionDefinition {
// maxCountArguments is the maximum amount of arguments the action accepts
uint8_t maxCountArguments = minCountArguments;
// possibleArguments is empty or contains all possible argument values,
// it is an ordered mapping from option name (what the user sees) to
// arguments (what the action code will see).
// As std::map<K, V> does not guarantee order this is a std::vector<...>
std::vector<std::pair<QString, std::vector<QString>>> possibleArguments =
{};
// When possibleArguments are present this should be a string like
// "Direction:" which will be shown before the values from
// possibleArguments in the UI. Otherwise, it should be empty.
QString argumentsPrompt = "";
// A more detailed description of what argumentsPrompt means
QString argumentsPromptHover = "";
};
using ActionDefinitionMap = std::map<QString, ActionDefinition>;
@ -39,15 +70,22 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
}},
{"scrollPage",
ActionDefinition{
"Scroll",
"<up or down>",
1,
.displayName = "Scroll",
.argumentDescription = "<direction: up or down>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments{
{"Up", {"up"}},
{"Down", {"down"}},
},
.argumentsPrompt = "Direction:",
}},
{"search", ActionDefinition{"Focus search box"}},
{"execModeratorAction",
ActionDefinition{
"Usercard: execute moderation action",
"<ban, unban or number of the timeout button to use>", 1}},
{"pin", ActionDefinition{"Usercard, reply thread: pin window"}},
}},
{HotkeyCategory::Split,
{
@ -57,24 +95,42 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{"delete", ActionDefinition{"Close"}},
{"focus",
ActionDefinition{
"Focus neighbouring split",
"<up, down, left, or right>",
1,
.displayName = "Focus neighbouring split",
.argumentDescription = "<direction: up, down, left or right>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments{
{"Up", {"up"}},
{"Down", {"down"}},
{"Left", {"left"}},
{"Right", {"right"}},
},
.argumentsPrompt = "Direction:",
.argumentsPromptHover =
"Which direction to look for a split to focus?",
}},
{"openInBrowser", ActionDefinition{"Open channel in browser"}},
{"openInCustomPlayer",
ActionDefinition{"Open stream in custom player"}},
{"openInStreamlink", ActionDefinition{"Open stream in streamlink"}},
{"openModView", ActionDefinition{"Open mod view in browser"}},
{"openViewerList", ActionDefinition{"Open viewer list"}},
{"openViewerList", ActionDefinition{"Open chatter list"}},
{"pickFilters", ActionDefinition{"Pick filters"}},
{"reconnect", ActionDefinition{"Reconnect to chat"}},
{"reloadEmotes",
ActionDefinition{
"Reload emotes",
"[channel or subscriber]",
0,
1,
.displayName = "Reload emotes",
.argumentDescription =
"[type: channel or subscriber; default: all emotes]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments{
{"All emotes", {}},
{"Channel emotes only", {"channel"}},
{"Subscriber emotes only", {"subscriber"}},
},
.argumentsPrompt = "Emote type:",
.argumentsPromptHover = "Which emotes should Chatterino reload",
}},
{"runCommand",
ActionDefinition{
@ -84,25 +140,41 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
}},
{"scrollPage",
ActionDefinition{
"Scroll",
"<up or down>",
1,
.displayName = "Scroll",
.argumentDescription = "<up or down>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments{
{"Up", {"up"}},
{"Down", {"down"}},
},
.argumentsPrompt = "Direction:",
.argumentsPromptHover =
"Which direction do you want to see more messages",
}},
{"scrollToBottom", ActionDefinition{"Scroll to the bottom"}},
{"scrollToTop", ActionDefinition{"Scroll to the top"}},
{"setChannelNotification",
ActionDefinition{
"Set channel live notification",
"[on or off. default: toggle]",
0,
1,
.displayName = "Set channel live notification",
.argumentDescription = "[on or off. default: toggle]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE,
.argumentsPrompt = "New value:",
.argumentsPromptHover = "Should the channel live notification be "
"enabled, disabled or toggled",
}},
{"setModerationMode",
ActionDefinition{
"Set moderation mode",
"[on or off. default: toggle]",
0,
1,
.displayName = "Set moderation mode",
.argumentDescription = "[on or off. default: toggle]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments = HOTKEY_ARG_ON_OFF_TOGGLE,
.argumentsPrompt = "New value:",
.argumentsPromptHover =
"Should the moderation mode be enabled, disabled or toggled",
}},
{"showSearch", ActionDefinition{"Search current channel"}},
{"showGlobalSearch", ActionDefinition{"Search all channels"}},
@ -114,21 +186,38 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{"clear", ActionDefinition{"Clear message"}},
{"copy",
ActionDefinition{
"Copy",
"<source of text: split, splitInput or auto>",
1,
.displayName = "Copy",
.argumentDescription =
"<source of text: auto, split or splitInput>",
.minCountArguments = 1,
.possibleArguments{
{"Automatic", {"auto"}},
{"Split", {"split"}},
{"Split Input", {"splitInput"}},
},
.argumentsPrompt = "Source of text:",
}},
{"cursorToStart",
ActionDefinition{
"To start of message",
"<withSelection or withoutSelection>",
1,
.displayName = "To start of message",
.argumentDescription =
"<selection mode: withSelection or withoutSelection>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION,
.argumentsPrompt = "Select text from cursor to start:",
// XXX: write a hover for this that doesn't suck
}},
{"cursorToEnd",
ActionDefinition{
"To end of message",
"<withSelection or withoutSelection>",
1,
.displayName = "To end of message",
.argumentDescription =
"<selection mode: withSelection or withoutSelection>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments = HOTKEY_ARG_WITH_OR_WITHOUT_SELECTION,
.argumentsPrompt = "Select text from cursor to end:",
// XXX: write a hover for this that doesn't suck
}},
{"nextMessage", ActionDefinition{"Choose next sent message"}},
{"openEmotesPopup", ActionDefinition{"Open emotes list"}},
@ -140,10 +229,16 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{"selectWord", ActionDefinition{"Select word"}},
{"sendMessage",
ActionDefinition{
"Send message",
"[keepInput to not clear the text after sending]",
0,
1,
.displayName = "Send message",
.argumentDescription =
"[keepInput to not clear the text after sending]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments{
{"Default behavior", {}},
{"Keep message in input after sending it", {"keepInput"}},
},
.argumentsPrompt = "Behavior:",
}},
{"undo", ActionDefinition{"Undo"}},
@ -163,7 +258,7 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{"moveTab",
ActionDefinition{
"Move tab",
"<next, previous, or new index of tab>",
"<where to move the tab: next, previous, or new index of tab>",
1,
}},
{"newSplit", ActionDefinition{"Create a new split"}},
@ -172,40 +267,78 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
{"openTab",
ActionDefinition{
"Select tab",
"<last, next, previous, or index of tab to select>",
"<which tab to select: last, next, previous, or index>",
1,
}},
{"openQuickSwitcher", ActionDefinition{"Open the quick switcher"}},
{"popup",
ActionDefinition{
"New popup",
"<split or window>",
1,
.displayName = "New popup",
.argumentDescription = "<split or window>",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments{
{"Focused Split", {"split"}},
{"Entire Tab", {"window"}},
},
.argumentsPrompt = "Include:",
.argumentsPromptHover =
"What should be included in the new popup",
}},
{"quit", ActionDefinition{"Quit Chatterino"}},
{"removeTab", ActionDefinition{"Remove current tab"}},
{"reopenSplit", ActionDefinition{"Reopen closed split"}},
{"setStreamerMode",
ActionDefinition{
"Set streamer mode",
"[on, off, toggle, or auto. default: toggle]",
0,
1,
.displayName = "Set streamer mode",
.argumentDescription =
"[on, off, toggle, or auto. default: toggle]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments =
{
{"Toggle on/off", {}},
{"Set to on", {"on"}},
{"Set to off", {"off"}},
{"Set to automatic", {"auto"}},
},
.argumentsPrompt = "New value:",
.argumentsPromptHover =
"Should streamer mode be enabled, disabled, toggled (on/off) "
"or set to auto",
}},
{"toggleLocalR9K", ActionDefinition{"Toggle local R9K"}},
{"zoom",
ActionDefinition{
"Zoom in/out",
"<in, out, or reset>",
1,
.displayName = "Zoom in/out",
.argumentDescription = "Argument:",
.minCountArguments = 1,
.maxCountArguments = 1,
.possibleArguments =
{
{"Zoom in", {"in"}},
{"Zoom out", {"out"}},
{"Reset zoom", {"reset"}},
},
.argumentsPrompt = "Option:",
}},
{"setTabVisibility",
ActionDefinition{
"Set tab visibility",
"[on, off, or toggle. default: toggle]",
0,
1,
}}}},
.displayName = "Set tab visibility",
.argumentDescription = "[on, off, toggle, liveOnly, or "
"toggleLiveOnly. default: toggle]",
.minCountArguments = 0,
.maxCountArguments = 1,
.possibleArguments{{"Toggle", {}},
{"Set to on", {"on"}},
{"Set to off", {"off"}},
{"Live only on", {"liveOnly"}},
{"Live only toggle", {"toggleLiveOnly"}}},
.argumentsPrompt = "New value:",
.argumentsPromptHover = "Should the tabs be enabled, disabled, "
"toggled, or live-only.",
}},
}},
};
} // namespace chatterino

View file

@ -500,6 +500,10 @@ void HotkeyController::addDefaults(std::set<QString> &addedHotkeys)
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+U"), "setTabVisibility",
{"toggle"}, "toggle tab visibility");
this->tryAddDefault(addedHotkeys, HotkeyCategory::Window,
QKeySequence("Ctrl+Shift+L"), "setTabVisibility",
{"toggleLiveOnly"}, "toggle live tabs only");
}
}

View file

@ -1,5 +1,9 @@
#include "controllers/hotkeys/HotkeyHelpers.hpp"
#include "controllers/hotkeys/ActionNames.hpp"
#include "controllers/hotkeys/HotkeyCategory.hpp"
#include <boost/optional/optional.hpp>
#include <QStringList>
namespace chatterino {
@ -27,4 +31,20 @@ std::vector<QString> parseHotkeyArguments(QString argumentString)
return arguments;
}
boost::optional<ActionDefinition> findHotkeyActionDefinition(
HotkeyCategory category, const QString &action)
{
auto allActions = actionNames.find(category);
if (allActions != actionNames.end())
{
const auto &actionsMap = allActions->second;
auto definition = actionsMap.find(action);
if (definition != actionsMap.end())
{
return {definition->second};
}
}
return {};
}
} // namespace chatterino

View file

@ -1,5 +1,8 @@
#pragma once
#include "controllers/hotkeys/ActionNames.hpp"
#include <boost/optional/optional.hpp>
#include <QString>
#include <vector>
@ -7,5 +10,7 @@
namespace chatterino {
std::vector<QString> parseHotkeyArguments(QString argumentString);
boost::optional<ActionDefinition> findHotkeyActionDefinition(
HotkeyCategory category, const QString &action);
} // namespace chatterino

View file

@ -32,10 +32,10 @@ bool isIgnoredMessage(IgnoredMessageParameters &&params)
{
auto sourceUserID = params.twitchUserID;
auto blocks =
getApp()->accounts->twitch.getCurrent()->accessBlockedUserIds();
if (auto it = blocks->find(sourceUserID); it != blocks->end())
bool isBlocked =
getApp()->accounts->twitch.getCurrent()->blockedUserIds().contains(
sourceUserID);
if (isBlocked)
{
switch (static_cast<ShowIgnoredUsersMessages>(
getSettings()->showBlockedUsersMessages.getValue()))

View file

@ -3,6 +3,7 @@
#include "util/RapidjsonHelpers.hpp"
#include "util/RapidJsonSerializeQString.hpp"
#include <boost/optional.hpp>
#include <pajlada/serialize.hpp>
#include <QRegularExpression>
#include <QString>
@ -58,25 +59,25 @@ public:
return this->isCaseSensitive_;
}
[[nodiscard]] bool match(QString &usernameText) const
[[nodiscard]] boost::optional<QString> match(
const QString &usernameText) const
{
if (this->isRegex())
{
if (!this->regex_.isValid())
{
return false;
return boost::none;
}
if (this->name().isEmpty())
{
return false;
return boost::none;
}
auto workingCopy = usernameText;
workingCopy.replace(this->regex_, this->replace());
if (workingCopy != usernameText)
{
usernameText = workingCopy;
return true;
return workingCopy;
}
}
else
@ -85,12 +86,11 @@ public:
this->name().compare(usernameText, this->caseSensitivity());
if (res == 0)
{
usernameText = this->replace();
return true;
return this->replace();
}
}
return false;
return boost::none;
}
private:

View file

@ -20,7 +20,6 @@ constexpr const auto NUM_SOUNDS = 4;
SoundController::SoundController()
: context(std::make_unique<ma_context>())
, resourceManager(std::make_unique<ma_resource_manager>())
, device(std::make_unique<ma_device>())
, engine(std::make_unique<ma_engine>())
{
}
@ -66,27 +65,9 @@ void SoundController::initialize(Settings &settings, Paths &paths)
this->defaultPingData = defaultPingFile.readAll();
/// Initialize a sound device
auto deviceConfig = ma_device_config_init(ma_device_type_playback);
deviceConfig.playback.pDeviceID = nullptr;
deviceConfig.playback.format = this->resourceManager->config.decodedFormat;
deviceConfig.playback.channels = 0;
deviceConfig.pulse.pStreamNamePlayback = "Chatterino MA";
deviceConfig.sampleRate = this->resourceManager->config.decodedSampleRate;
deviceConfig.dataCallback = ma_engine_data_callback_internal;
deviceConfig.pUserData = this->engine.get();
result =
ma_device_init(this->context.get(), &deviceConfig, this->device.get());
if (result != MA_SUCCESS)
if (!this->recreateDevice())
{
qCWarning(chatterinoSound) << "Error initializing device:" << result;
return;
}
result = ma_device_start(this->device.get());
if (result != MA_SUCCESS)
{
qCWarning(chatterinoSound) << "Error starting device:" << result;
qCWarning(chatterinoSound) << "Failed to create the initial device";
return;
}
@ -172,7 +153,11 @@ SoundController::~SoundController()
}
ma_engine_uninit(this->engine.get());
ma_device_uninit(this->device.get());
if (this->device)
{
ma_device_uninit(this->device.get());
this->device.reset();
}
ma_resource_manager_uninit(this->resourceManager.get());
ma_context_uninit(this->context.get());
}
@ -204,7 +189,12 @@ void SoundController::play(const QUrl &sound)
{
qCWarning(chatterinoSound)
<< "Failed to start the sound device" << result;
return;
if (!this->recreateDevice())
{
qCWarning(chatterinoSound) << "Failed to recreate device";
return;
}
}
qCInfo(chatterinoSound) << "Successfully restarted the sound device";
@ -234,4 +224,44 @@ void SoundController::play(const QUrl &sound)
}
}
bool SoundController::recreateDevice()
{
ma_result result{};
if (this->device)
{
// Release the previous device first
qCDebug(chatterinoSound) << "Uniniting previously created device";
ma_device_uninit(this->device.get());
}
this->device = std::make_unique<ma_device>();
auto deviceConfig = ma_device_config_init(ma_device_type_playback);
deviceConfig.playback.pDeviceID = nullptr;
deviceConfig.playback.format = this->resourceManager->config.decodedFormat;
deviceConfig.playback.channels = 0;
deviceConfig.pulse.pStreamNamePlayback = "Chatterino MA";
deviceConfig.sampleRate = this->resourceManager->config.decodedSampleRate;
deviceConfig.dataCallback = ma_engine_data_callback_internal;
deviceConfig.pUserData = this->engine.get();
result =
ma_device_init(this->context.get(), &deviceConfig, this->device.get());
if (result != MA_SUCCESS)
{
qCWarning(chatterinoSound) << "Error initializing device:" << result;
return false;
}
result = ma_device_start(this->device.get());
if (result != MA_SUCCESS)
{
qCWarning(chatterinoSound) << "Error starting device:" << result;
return false;
}
return true;
}
} // namespace chatterino

View file

@ -45,7 +45,7 @@ private:
// Used for storing & reusing sounds to be played
std::unique_ptr<ma_resource_manager> resourceManager;
// The sound device we're playing sound into
std::unique_ptr<ma_device> device;
std::unique_ptr<ma_device> device{nullptr};
// The engine is a high-level API for playing sounds from paths in a simple & efficient-enough manner
std::unique_ptr<ma_engine> engine;
@ -64,6 +64,13 @@ private:
bool initialized{false};
// Recreates the sound device
// This is used during initialization, and can also be used if the device
// needs to be recreated during playback
//
// Returns false on failure
bool recreateDevice();
friend class Application;
};

View file

@ -0,0 +1,190 @@
#include "controllers/twitch/LiveController.hpp"
#include "common/QLogging.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "util/Helpers.hpp"
#include <QDebug>
namespace {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
const auto &LOG = chatterinoTwitchLiveController;
} // namespace
namespace chatterino {
TwitchLiveController::TwitchLiveController()
{
QObject::connect(&this->refreshTimer, &QTimer::timeout, [this] {
this->request();
});
this->refreshTimer.start(TwitchLiveController::REFRESH_INTERVAL);
QObject::connect(&this->immediateRequestTimer, &QTimer::timeout, [this] {
QStringList channelIDs;
{
std::unique_lock immediateRequestsLock(
this->immediateRequestsMutex);
for (const auto &channelID : this->immediateRequests)
{
channelIDs.append(channelID);
}
this->immediateRequests.clear();
}
if (channelIDs.isEmpty())
{
return;
}
this->request(channelIDs);
});
this->immediateRequestTimer.start(
TwitchLiveController::IMMEDIATE_REQUEST_INTERVAL);
}
void TwitchLiveController::add(const std::shared_ptr<TwitchChannel> &newChannel)
{
assert(newChannel != nullptr);
const auto channelID = newChannel->roomId();
assert(!channelID.isEmpty());
{
std::unique_lock lock(this->channelsMutex);
this->channels[channelID] = newChannel;
}
{
std::unique_lock immediateRequestsLock(this->immediateRequestsMutex);
this->immediateRequests.emplace(channelID);
}
}
void TwitchLiveController::request(std::optional<QStringList> optChannelIDs)
{
QStringList channelIDs;
if (optChannelIDs)
{
channelIDs = *optChannelIDs;
}
else
{
std::shared_lock lock(this->channelsMutex);
for (const auto &channelList : this->channels)
{
channelIDs.append(channelList.first);
}
}
if (channelIDs.isEmpty())
{
return;
}
auto batches =
splitListIntoBatches(channelIDs, TwitchLiveController::BATCH_SIZE);
qCDebug(LOG) << "Make" << batches.size() << "requests";
for (const auto &batch : batches)
{
// TODO: Explore making this concurrent
getHelix()->fetchStreams(
batch, {},
[this, batch{batch}](const auto &streams) {
std::unordered_map<QString, std::optional<HelixStream>> results;
for (const auto &channelID : batch)
{
results[channelID] = std::nullopt;
}
for (const auto &stream : streams)
{
results[stream.userId] = stream;
}
QStringList deadChannels;
{
std::shared_lock lock(this->channelsMutex);
for (const auto &result : results)
{
auto it = this->channels.find(result.first);
if (it != channels.end())
{
if (auto channel = it->second.lock(); channel)
{
channel->updateStreamStatus(result.second);
}
else
{
deadChannels.append(result.first);
}
}
}
}
if (!deadChannels.isEmpty())
{
std::unique_lock lock(this->channelsMutex);
for (const auto &deadChannel : deadChannels)
{
this->channels.erase(deadChannel);
}
}
},
[] {
qCWarning(LOG) << "Failed stream check request";
},
[] {});
// TODO: Explore making this concurrent
getHelix()->fetchChannels(
batch,
[this, batch{batch}](const auto &helixChannels) {
QStringList deadChannels;
{
std::shared_lock lock(this->channelsMutex);
for (const auto &helixChannel : helixChannels)
{
auto it = this->channels.find(helixChannel.userId);
if (it != this->channels.end())
{
if (auto channel = it->second.lock(); channel)
{
channel->updateStreamTitle(helixChannel.title);
channel->updateDisplayName(helixChannel.name);
}
else
{
deadChannels.append(helixChannel.userId);
}
}
}
}
if (!deadChannels.isEmpty())
{
std::unique_lock lock(this->channelsMutex);
for (const auto &deadChannel : deadChannels)
{
this->channels.erase(deadChannel);
}
}
},
[] {
qCWarning(LOG) << "Failed stream check request";
});
}
}
} // namespace chatterino

View file

@ -0,0 +1,89 @@
#pragma once
#include "common/Singleton.hpp"
#include "util/QStringHash.hpp"
#include <QString>
#include <QTimer>
#include <chrono>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <unordered_set>
namespace chatterino {
class TwitchChannel;
class ITwitchLiveController
{
public:
virtual ~ITwitchLiveController() = default;
virtual void add(const std::shared_ptr<TwitchChannel> &newChannel) = 0;
};
class TwitchLiveController : public ITwitchLiveController, public Singleton
{
public:
// Controls how often all channels have their stream status refreshed
static constexpr std::chrono::seconds REFRESH_INTERVAL{30};
// Controls how quickly new channels have their stream status loaded
static constexpr std::chrono::seconds IMMEDIATE_REQUEST_INTERVAL{1};
/**
* How many channels to include in a single request
*
* Should not be more than 100
**/
static constexpr int BATCH_SIZE{100};
TwitchLiveController();
// Add a Twitch channel to be queried for live status
// A request is made within a few seconds if this is the first time this channel is added
void add(const std::shared_ptr<TwitchChannel> &newChannel) override;
private:
/**
* Run batched Helix Channels & Stream requests for channels
*
* If a list of channel IDs is passed to request, we only make a request for those channels
*
* If no list of channels is passed to request (the default behaviour), we make requests for all channels
* in the `channels` map.
**/
void request(std::optional<QStringList> optChannelIDs = std::nullopt);
/**
* List of channel IDs pointing to their Twitch Channel
*
* These channels will have their stream status updated every REFRESH_INTERVAL seconds
**/
std::unordered_map<QString, std::weak_ptr<TwitchChannel>> channels;
std::shared_mutex channelsMutex;
/**
* List of channels that need an immediate live status update
*
* These channels will have their stream status updated after at most IMMEDIATE_REQUEST_INTERVAL seconds
**/
std::unordered_set<QString> immediateRequests;
std::mutex immediateRequestsMutex;
/**
* Timer responsible for refreshing `channels`
**/
QTimer refreshTimer;
/**
* Timer responsible for refreshing `immediateRequests`
**/
QTimer immediateRequestTimer;
};
} // namespace chatterino

View file

@ -76,6 +76,8 @@ namespace detail {
60000);
}
this->processOffset();
DebugCount::increase("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever loaded)", this->memoryUsage());
}
Frames::~Frames()
@ -91,10 +93,27 @@ namespace detail {
{
DebugCount::decrease("animated images");
}
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)",
this->memoryUsage());
this->gifTimerConnection_.disconnect();
}
int64_t Frames::memoryUsage() const
{
int64_t usage = 0;
for (const auto &frame : this->items_)
{
auto sz = frame.image.size();
auto area = sz.width() * sz.height();
auto memory = area * frame.image.depth();
usage += memory;
}
return usage;
}
void Frames::advance()
{
this->durationOffset_ += GIF_FRAME_LENGTH;
@ -131,6 +150,9 @@ namespace detail {
{
DebugCount::decrease("loaded images");
}
DebugCount::decrease("image bytes", this->memoryUsage());
DebugCount::increase("image bytes (ever unloaded)",
this->memoryUsage());
this->items_.clear();
this->index_ = 0;
@ -573,8 +595,8 @@ ImageExpirationPool::ImageExpirationPool()
ImageExpirationPool &ImageExpirationPool::instance()
{
static ImageExpirationPool instance;
return instance;
static auto *instance = new ImageExpirationPool;
return *instance;
}
void ImageExpirationPool::addImagePtr(ImagePtr imgPtr)
@ -589,14 +611,26 @@ void ImageExpirationPool::removeImagePtr(Image *rawPtr)
this->allImages_.erase(rawPtr);
}
void ImageExpirationPool::freeAll()
{
{
std::lock_guard<std::mutex> lock(this->mutex_);
for (auto it = this->allImages_.begin(); it != this->allImages_.end();)
{
auto img = it->second.lock();
img->expireFrames();
it = this->allImages_.erase(it);
}
}
this->freeOld();
}
void ImageExpirationPool::freeOld()
{
std::lock_guard<std::mutex> lock(this->mutex_);
# ifndef NDEBUG
size_t numExpired = 0;
size_t eligible = 0;
# endif
auto now = std::chrono::steady_clock::now();
for (auto it = this->allImages_.begin(); it != this->allImages_.end();)
@ -617,17 +651,13 @@ void ImageExpirationPool::freeOld()
continue;
}
# ifndef NDEBUG
++eligible;
# endif
// Check if image has expired and, if so, expire its frame data
auto diff = now - img->lastUsed_;
if (diff > IMAGE_POOL_IMAGE_LIFETIME)
{
# ifndef NDEBUG
++numExpired;
# endif
img->expireFrames();
// erase without mutex locking issue
it = this->allImages_.erase(it);
@ -641,6 +671,9 @@ void ImageExpirationPool::freeOld()
qCDebug(chatterinoImage) << "freed frame data for" << numExpired << "/"
<< eligible << "eligible images";
# endif
DebugCount::set("last image gc: expired", numExpired);
DebugCount::set("last image gc: eligible", eligible);
DebugCount::set("last image gc: left after gc", this->allImages_.size());
}
#endif

View file

@ -41,6 +41,7 @@ namespace detail {
boost::optional<QPixmap> first() const;
private:
int64_t memoryUsage() const;
void processOffset();
QVector<Frame<QPixmap>> items_;
int index_{0};
@ -111,6 +112,7 @@ class ImageExpirationPool
{
private:
friend class Image;
friend class CommandController;
ImageExpirationPool();
static ImageExpirationPool &instance();
@ -126,6 +128,12 @@ private:
*/
void freeOld();
/*
* Debug function that unloads all images in the pool. This is intended to
* test for possible memory leaks from tracked images.
*/
void freeAll();
private:
// Timer to periodically run freeOld()
QTimer *freeTimer_;

View file

@ -46,7 +46,7 @@ enum class MessageFlag : int64_t {
FirstMessage = (1LL << 23),
ReplyMessage = (1LL << 24),
ElevatedMessage = (1LL << 25),
ParticipatedThread = (1LL << 26),
SubscribedThread = (1LL << 26),
CheerMessage = (1LL << 27),
LiveUpdatesAdd = (1LL << 28),
LiveUpdatesRemove = (1LL << 29),

View file

@ -1,4 +1,4 @@
#include "MessageThread.hpp"
#include "messages/MessageThread.hpp"
#include "messages/Message.hpp"
#include "util/DebugCount.hpp"
@ -58,14 +58,26 @@ size_t MessageThread::liveCount(
return count;
}
bool MessageThread::participated() const
void MessageThread::markSubscribed()
{
return this->participated_;
if (this->subscription_ == Subscription::Subscribed)
{
return;
}
this->subscription_ = Subscription::Subscribed;
this->subscriptionUpdated();
}
void MessageThread::markParticipated()
void MessageThread::markUnsubscribed()
{
this->participated_ = true;
if (this->subscription_ == Subscription::Unsubscribed)
{
return;
}
this->subscription_ = Subscription::Unsubscribed;
this->subscriptionUpdated();
}
} // namespace chatterino

View file

@ -1,5 +1,6 @@
#pragma once
#include <boost/signals2.hpp>
#include <QString>
#include <memory>
@ -11,6 +12,12 @@ struct Message;
class MessageThread
{
public:
enum class Subscription : uint8_t {
None,
Subscribed,
Unsubscribed,
};
MessageThread(std::shared_ptr<const Message> rootMessage);
~MessageThread();
@ -23,9 +30,22 @@ public:
/// Returns the number of live reply references
size_t liveCount(const std::shared_ptr<const Message> &exclude) const;
bool participated() const;
bool subscribed() const
{
return this->subscription_ == Subscription::Subscribed;
}
void markParticipated();
/// Returns true if and only if the user manually unsubscribed from the thread
/// @see #markUnsubscribed()
bool unsubscribed() const
{
return this->subscription_ == Subscription::Unsubscribed;
}
/// Subscribe to this thread.
void markSubscribed();
/// Unsubscribe from this thread.
void markUnsubscribed();
const QString &rootId() const
{
@ -42,11 +62,14 @@ public:
return replies_;
}
boost::signals2::signal<void()> subscriptionUpdated;
private:
const QString rootMessageId_;
const std::shared_ptr<const Message> rootMessage_;
std::vector<std::weak_ptr<const Message>> replies_;
bool participated_ = false;
Subscription subscription_ = Subscription::None;
};
} // namespace chatterino

View file

@ -78,6 +78,7 @@ struct Selection {
if (offset > this->selectionMin.messageIndex)
{
this->selectionMin.messageIndex = 0;
this->selectionMin.charIndex = 0;
}
else
{
@ -87,6 +88,7 @@ struct Selection {
if (offset > this->selectionMax.messageIndex)
{
this->selectionMax.messageIndex = 0;
this->selectionMax.charIndex = 0;
}
else
{
@ -96,6 +98,7 @@ struct Selection {
if (offset > this->start.messageIndex)
{
this->start.messageIndex = 0;
this->start.charIndex = 0;
}
else
{
@ -105,6 +108,7 @@ struct Selection {
if (offset > this->end.messageIndex)
{
this->end.messageIndex = 0;
this->end.charIndex = 0;
}
else
{

View file

@ -270,14 +270,9 @@ QString SharedMessageBuilder::stylizeUsername(const QString &username,
break;
}
auto nicknames = getCSettings().nicknames.readOnly();
for (const auto &nickname : *nicknames)
if (auto nicknameText = getCSettings().matchNickname(usernameText))
{
if (nickname.match(usernameText))
{
break;
}
usernameText = *nicknameText;
}
return usernameText;

View file

@ -1,16 +1,14 @@
#include "messages/layouts/MessageLayout.hpp"
#include "Application.hpp"
#include "debug/Benchmark.hpp"
#include "messages/layouts/MessageLayoutContainer.hpp"
#include "messages/layouts/MessageLayoutContext.hpp"
#include "messages/layouts/MessageLayoutElement.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
#include "messages/Selection.hpp"
#include "providers/colors/ColorProvider.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
#include "util/DebugCount.hpp"
#include "util/StreamerMode.hpp"
@ -198,84 +196,77 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
}
// Painting
void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage,
bool isWindowFocused, bool isMentions)
void MessageLayout::paint(const MessagePaintContext &ctx)
{
auto app = getApp();
QPixmap *pixmap = this->ensureBuffer(painter, width);
QPixmap *pixmap = this->ensureBuffer(ctx.painter, ctx.canvasWidth);
if (!this->bufferValid_ || !selection.isEmpty())
if (!this->bufferValid_ || !ctx.selection.isEmpty())
{
this->updateBuffer(pixmap, messageIndex, selection);
this->updateBuffer(pixmap, ctx);
}
// draw on buffer
painter.drawPixmap(0, y, *pixmap);
// painter.drawPixmap(0, y, this->container.width,
// this->container.getHeight(), *pixmap);
ctx.painter.drawPixmap(0, ctx.y, *pixmap);
// draw gif emotes
this->container_.paintAnimatedElements(painter, y);
this->container_.paintAnimatedElements(ctx.painter, ctx.y);
// draw disabled
if (this->message_->flags.has(MessageFlag::Disabled))
{
painter.fillRect(0, y, pixmap->width(), pixmap->height(),
app->themes->messages.disabled);
// painter.fillRect(0, y, pixmap->width(), pixmap->height(),
// QBrush(QColor(64, 64, 64, 64)));
ctx.painter.fillRect(0, ctx.y, pixmap->width(), pixmap->height(),
ctx.messageColors.disabled);
}
if (this->message_->flags.has(MessageFlag::RecentMessage))
{
painter.fillRect(0, y, pixmap->width(), pixmap->height(),
app->themes->messages.disabled);
ctx.painter.fillRect(0, ctx.y, pixmap->width(), pixmap->height(),
ctx.messageColors.disabled);
}
if (!isMentions &&
if (!ctx.isMentions &&
(this->message_->flags.has(MessageFlag::RedeemedChannelPointReward) ||
this->message_->flags.has(MessageFlag::RedeemedHighlight)) &&
getSettings()->enableRedeemedHighlight.getValue())
ctx.preferences.enableRedeemedHighlight)
{
painter.fillRect(
0, y, this->scale_ * 4, pixmap->height(),
ctx.painter.fillRect(
0, ctx.y, int(this->scale_ * 4), pixmap->height(),
*ColorProvider::instance().color(ColorType::RedeemedHighlight));
}
// draw selection
if (!selection.isEmpty())
if (!ctx.selection.isEmpty())
{
this->container_.paintSelection(painter, messageIndex, selection, y);
this->container_.paintSelection(ctx.painter, ctx.messageIndex,
ctx.selection, ctx.y);
}
// draw message seperation line
if (getSettings()->separateMessages.getValue())
if (ctx.preferences.separateMessages)
{
painter.fillRect(0, y, this->container_.getWidth() + 64, 1,
app->themes->splits.messageSeperator);
ctx.painter.fillRect(0, ctx.y, this->container_.getWidth() + 64, 1,
ctx.messageColors.messageSeperator);
}
// draw last read message line
if (isLastReadMessage)
if (ctx.isLastReadMessage)
{
QColor color;
if (getSettings()->lastMessageColor != QStringLiteral(""))
if (ctx.preferences.lastMessageColor.isValid())
{
color = QColor(getSettings()->lastMessageColor.getValue());
color = ctx.preferences.lastMessageColor;
}
else
{
color = isWindowFocused
? app->themes->tabs.selected.backgrounds.regular
: app->themes->tabs.selected.backgrounds.unfocused;
color = ctx.isWindowFocused
? ctx.messageColors.focusedLastMessageLine
: ctx.messageColors.unfocusedLastMessageLine;
}
QBrush brush(color, static_cast<Qt::BrushStyle>(
getSettings()->lastMessagePattern.getValue()));
QBrush brush(color, ctx.preferences.lastMessagePattern);
painter.fillRect(0, y + this->container_.getHeight() - 1,
pixmap->width(), 1, brush);
ctx.painter.fillRect(0, ctx.y + this->container_.getHeight() - 1,
pixmap->width(), 1, brush);
}
this->bufferValid_ = true;
@ -305,45 +296,42 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
return this->buffer_.get();
}
void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
Selection & /*selection*/)
void MessageLayout::updateBuffer(QPixmap *buffer,
const MessagePaintContext &ctx)
{
if (buffer->isNull())
{
return;
auto app = getApp();
auto settings = getSettings();
}
QPainter painter(buffer);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
// draw background
QColor backgroundColor = [this, &app] {
if (getSettings()->alternateMessages.getValue() &&
QColor backgroundColor = [&] {
if (ctx.preferences.alternateMessages &&
this->flags.has(MessageLayoutFlag::AlternateBackground))
{
return app->themes->messages.backgrounds.alternate;
}
else
{
return app->themes->messages.backgrounds.regular;
return ctx.messageColors.alternate;
}
return ctx.messageColors.regular;
}();
if (this->message_->flags.has(MessageFlag::ElevatedMessage) &&
getSettings()->enableElevatedMessageHighlight.getValue())
{
backgroundColor = blendColors(backgroundColor,
*ColorProvider::instance().color(
ColorType::ElevatedMessageHighlight));
}
else if (this->message_->flags.has(MessageFlag::FirstMessage) &&
getSettings()->enableFirstMessageHighlight.getValue())
ctx.preferences.enableElevatedMessageHighlight)
{
backgroundColor = blendColors(
backgroundColor,
*ColorProvider::instance().color(ColorType::FirstMessageHighlight));
*ctx.colorProvider.color(ColorType::ElevatedMessageHighlight));
}
else if (this->message_->flags.has(MessageFlag::FirstMessage) &&
ctx.preferences.enableFirstMessageHighlight)
{
backgroundColor = blendColors(
backgroundColor,
*ctx.colorProvider.color(ColorType::FirstMessageHighlight));
}
else if ((this->message_->flags.has(MessageFlag::Highlighted) ||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
@ -354,22 +342,21 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
blendColors(backgroundColor, *this->message_->highlightColor);
}
else if (this->message_->flags.has(MessageFlag::Subscription) &&
getSettings()->enableSubHighlight)
ctx.preferences.enableSubHighlight)
{
// Blend highlight color with usual background color
backgroundColor = blendColors(
backgroundColor,
*ColorProvider::instance().color(ColorType::Subscription));
backgroundColor, *ctx.colorProvider.color(ColorType::Subscription));
}
else if ((this->message_->flags.has(MessageFlag::RedeemedHighlight) ||
this->message_->flags.has(
MessageFlag::RedeemedChannelPointReward)) &&
settings->enableRedeemedHighlight.getValue())
ctx.preferences.enableRedeemedHighlight)
{
// Blend highlight color with usual background color
backgroundColor = blendColors(
backgroundColor,
*ColorProvider::instance().color(ColorType::RedeemedHighlight));
backgroundColor =
blendColors(backgroundColor,
*ctx.colorProvider.color(ColorType::RedeemedHighlight));
}
else if (this->message_->flags.has(MessageFlag::AutoMod))
{
@ -383,7 +370,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
painter.fillRect(buffer->rect(), backgroundColor);
// draw message
this->container_.paintElements(painter);
this->container_.paintElements(painter, ctx);
#ifdef FOURTF
// debug

View file

@ -18,6 +18,7 @@ using MessagePtr = std::shared_ptr<const Message>;
struct Selection;
struct MessageLayoutContainer;
class MessageLayoutElement;
struct MessagePaintContext;
enum class MessageElementFlag : int64_t;
using MessageElementFlags = FlagsEnum<MessageElementFlag>;
@ -49,9 +50,7 @@ public:
bool layout(int width, float scale_, MessageElementFlags flags);
// Painting
void paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage,
bool isWindowFocused, bool isMentions);
void paint(const MessagePaintContext &ctx);
void invalidateBuffer();
void deleteBuffer();
void deleteCache();
@ -72,7 +71,7 @@ public:
private:
// methods
void actuallyLayout(int width, MessageElementFlags flags);
void updateBuffer(QPixmap *pixmap, int messageIndex, Selection &selection);
void updateBuffer(QPixmap *buffer, const MessagePaintContext &ctx);
// Create new buffer if required, returning the buffer
QPixmap *ensureBuffer(QPainter &painter, int width);

View file

@ -1,6 +1,7 @@
#include "MessageLayoutContainer.hpp"
#include "Application.hpp"
#include "messages/layouts/MessageLayoutContext.hpp"
#include "messages/layouts/MessageLayoutElement.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
@ -151,7 +152,7 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
}
// top margin
if (this->elements_.size() == 0)
if (this->elements_.empty())
{
this->currentY_ = int(this->margin.top * this->scale_);
}
@ -276,17 +277,24 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
// 2 - in LTR mode, the previous word should be RTL (i.e. reversed)
for (int i = startIndex; i <= endIndex; i++)
{
if (isNeutral(this->elements_[i]->getText()) &&
auto &element = this->elements_[i];
const auto neutral = isNeutral(element->getText());
const auto neutralOrUsername =
neutral ||
element->getFlags().hasAny({MessageElementFlag::BoldUsername,
MessageElementFlag::NonBoldUsername});
if (neutral &&
((this->first == FirstWord::RTL && !this->wasPrevReversed_) ||
(this->first == FirstWord::LTR && this->wasPrevReversed_)))
{
this->elements_[i]->reversedNeutral = true;
element->reversedNeutral = true;
}
if (((this->elements_[i]->getText().isRightToLeft() !=
if (((element->getText().isRightToLeft() !=
(this->first == FirstWord::RTL)) &&
!isNeutral(this->elements_[i]->getText())) ||
(isNeutral(this->elements_[i]->getText()) &&
this->wasPrevReversed_))
!neutralOrUsername) ||
(neutralOrUsername && this->wasPrevReversed_))
{
swappedSequence.push(i);
this->wasPrevReversed_ = true;
@ -379,7 +387,7 @@ void MessageLayoutContainer::breakLine()
element->getRect().y() + this->lineHeight_ + yExtra));
}
if (this->lines_.size() != 0)
if (!this->lines_.empty())
{
this->lines_.back().endIndex = this->lineStart_;
this->lines_.back().endCharIndex = this->charIndex_;
@ -388,7 +396,7 @@ void MessageLayoutContainer::breakLine()
{(int)lineStart_, 0, this->charIndex_, 0,
QRect(-100000, this->currentY_, 200000, lineHeight_)});
for (int i = this->lineStart_; i < this->elements_.size(); i++)
for (auto i = this->lineStart_; i < this->elements_.size(); i++)
{
this->charIndex_ += this->elements_[i]->getSelectionIndexCount();
}
@ -458,7 +466,7 @@ void MessageLayoutContainer::end()
this->height_ += this->lineHeight_;
if (this->lines_.size() != 0)
if (!this->lines_.empty())
{
this->lines_[0].rect.setTop(-100000);
this->lines_.back().rect.setBottom(100000);
@ -473,7 +481,7 @@ bool MessageLayoutContainer::canCollapse()
this->flags_.has(MessageFlag::Collapsed);
}
bool MessageLayoutContainer::isCollapsed()
bool MessageLayoutContainer::isCollapsed() const
{
return this->isCollapsed_;
}
@ -492,7 +500,8 @@ MessageLayoutElement *MessageLayoutContainer::getElementAt(QPoint point)
}
// painting
void MessageLayoutContainer::paintElements(QPainter &painter)
void MessageLayoutContainer::paintElements(QPainter &painter,
const MessagePaintContext &ctx)
{
for (const std::unique_ptr<MessageLayoutElement> &element : this->elements_)
{
@ -501,7 +510,7 @@ void MessageLayoutContainer::paintElements(QPainter &painter)
painter.drawRect(element->getRect());
#endif
element->paint(painter);
element->paint(painter, ctx.messageColors);
}
}
@ -514,10 +523,12 @@ void MessageLayoutContainer::paintAnimatedElements(QPainter &painter,
}
}
void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
Selection &selection, int yOffset)
void MessageLayoutContainer::paintSelection(QPainter &painter,
size_t messageIndex,
const Selection &selection,
int yOffset)
{
auto app = getApp();
auto *app = getApp();
QColor selectionColor = app->themes->messages.selection;
// don't draw anything
@ -706,7 +717,7 @@ void MessageLayoutContainer::paintSelection(QPainter &painter, int messageIndex,
// selection
int MessageLayoutContainer::getSelectionIndex(QPoint point)
{
if (this->elements_.size() == 0)
if (this->elements_.empty())
{
return 0;
}
@ -767,7 +778,7 @@ int MessageLayoutContainer::getSelectionIndex(QPoint point)
// fourtf: no idea if this is acurate LOL
int MessageLayoutContainer::getLastCharacterIndex() const
{
if (this->lines_.size() == 0)
if (this->lines_.empty())
{
return 0;
}
@ -784,7 +795,7 @@ int MessageLayoutContainer::getFirstMessageCharacterIndex() const
// Get the index of the first character of the real message
int index = 0;
for (auto &element : this->elements_)
for (const auto &element : this->elements_)
{
if (element->getFlags().hasAny(skippedFlags))
{
@ -846,10 +857,8 @@ void MessageLayoutContainer::addSelectionText(QString &str, uint32_t from,
element->addCopyTextToString(str, 0, to - index);
break;
}
else
{
element->addCopyTextToString(str);
}
element->addCopyTextToString(str);
}
index += indexCount;

View file

@ -18,6 +18,7 @@ enum class FirstWord { Neutral, RTL, LTR };
using MessageFlags = FlagsEnum<MessageFlag>;
class MessageLayoutElement;
struct Selection;
struct MessagePaintContext;
struct Margin {
int top;
@ -73,10 +74,10 @@ struct MessageLayoutContainer {
MessageLayoutElement *getElementAt(QPoint point);
// painting
void paintElements(QPainter &painter);
void paintElements(QPainter &painter, const MessagePaintContext &ctx);
void paintAnimatedElements(QPainter &painter, int yOffset);
void paintSelection(QPainter &painter, int messageIndex,
Selection &selection, int yOffset);
void paintSelection(QPainter &painter, size_t messageIndex,
const Selection &selection, int yOffset);
// selection
int getSelectionIndex(QPoint point);
@ -85,7 +86,7 @@ struct MessageLayoutContainer {
void addSelectionText(QString &str, uint32_t from, uint32_t to,
CopyMode copymode);
bool isCollapsed();
bool isCollapsed() const;
private:
struct Line {

View file

@ -0,0 +1,82 @@
#include "messages/layouts/MessageLayoutContext.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
namespace chatterino {
void MessageColors::applyTheme(Theme *theme)
{
this->regular = theme->messages.backgrounds.regular;
this->alternate = theme->messages.backgrounds.alternate;
this->disabled = theme->messages.disabled;
this->selection = theme->messages.selection;
this->system = theme->messages.textColors.system;
this->messageSeperator = theme->splits.messageSeperator;
this->focusedLastMessageLine = theme->tabs.selected.backgrounds.regular;
this->unfocusedLastMessageLine = theme->tabs.selected.backgrounds.unfocused;
}
void MessagePreferences::connectSettings(Settings *settings,
pajlada::Signals::SignalHolder &holder)
{
settings->enableRedeemedHighlight.connect(
[this](const auto &newValue) {
this->enableRedeemedHighlight = newValue;
},
holder);
settings->enableElevatedMessageHighlight.connect(
[this](const auto &newValue) {
this->enableElevatedMessageHighlight = newValue;
},
holder);
settings->enableFirstMessageHighlight.connect(
[this](const auto &newValue) {
this->enableFirstMessageHighlight = newValue;
},
holder);
settings->enableSubHighlight.connect(
[this](const auto &newValue) {
this->enableSubHighlight = newValue;
},
holder);
settings->alternateMessages.connect(
[this](const auto &newValue) {
this->alternateMessages = newValue;
},
holder);
settings->separateMessages.connect(
[this](const auto &newValue) {
this->separateMessages = newValue;
},
holder);
settings->lastMessageColor.connect(
[this](const auto &newValue) {
if (newValue.isEmpty())
{
this->lastMessageColor = QColor();
}
else
{
this->lastMessageColor = QColor(newValue);
}
},
holder);
settings->lastMessagePattern.connect(
[this](const auto &newValue) {
this->lastMessagePattern = static_cast<Qt::BrushStyle>(newValue);
},
holder);
}
} // namespace chatterino

View file

@ -0,0 +1,74 @@
#pragma once
#include <QColor>
#include <QPainter>
namespace pajlada::Signals {
class SignalHolder;
} // namespace pajlada::Signals
namespace chatterino {
class ColorProvider;
class Theme;
class Settings;
struct Selection;
// TODO: Figure out if this could be a subset of Theme instead (e.g. Theme::MessageColors)
struct MessageColors {
QColor regular;
QColor alternate;
QColor disabled;
QColor selection;
QColor system;
QColor messageSeperator;
QColor focusedLastMessageLine;
QColor unfocusedLastMessageLine;
void applyTheme(Theme *theme);
};
// TODO: Explore if we can let settings own this
struct MessagePreferences {
QColor lastMessageColor;
Qt::BrushStyle lastMessagePattern{};
bool enableRedeemedHighlight{};
bool enableElevatedMessageHighlight{};
bool enableFirstMessageHighlight{};
bool enableSubHighlight{};
bool alternateMessages{};
bool separateMessages{};
void connectSettings(Settings *settings,
pajlada::Signals::SignalHolder &holder);
};
struct MessagePaintContext {
QPainter &painter;
const Selection &selection;
const ColorProvider &colorProvider;
const MessageColors &messageColors;
const MessagePreferences &preferences;
// width of the area we have to draw on
const int canvasWidth{};
// whether the painting should be treated as if this view's window is focused
const bool isWindowFocused{};
// whether the painting should be treated as if this view is the special mentions view
const bool isMentions{};
// y coordinate we're currently painting at
int y{};
// Index of the message that is currently being painted
// This index refers to the snapshot being used in the painting
size_t messageIndex{};
bool isLastReadMessage{};
};
} // namespace chatterino

View file

@ -3,9 +3,9 @@
#include "Application.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/layouts/MessageLayoutContext.hpp"
#include "messages/MessageElement.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include "singletons/Theme.hpp"
#include "util/DebugCount.hpp"
#include <QDebug>
@ -137,7 +137,8 @@ int ImageLayoutElement::getSelectionIndexCount() const
return this->trailingSpace ? 2 : 1;
}
void ImageLayoutElement::paint(QPainter &painter)
void ImageLayoutElement::paint(QPainter &painter,
const MessageColors & /*messageColors*/)
{
if (this->image_ == nullptr)
{
@ -228,7 +229,8 @@ int LayeredImageLayoutElement::getSelectionIndexCount() const
return this->trailingSpace ? 2 : 1;
}
void LayeredImageLayoutElement::paint(QPainter &painter)
void LayeredImageLayoutElement::paint(QPainter &painter,
const MessageColors & /*messageColors*/)
{
auto fullRect = QRectF(this->getRect());
@ -329,7 +331,8 @@ ImageWithBackgroundLayoutElement::ImageWithBackgroundLayoutElement(
{
}
void ImageWithBackgroundLayoutElement::paint(QPainter &painter)
void ImageWithBackgroundLayoutElement::paint(
QPainter &painter, const MessageColors & /*messageColors*/)
{
if (this->image_ == nullptr)
{
@ -360,7 +363,8 @@ ImageWithCircleBackgroundLayoutElement::ImageWithCircleBackgroundLayoutElement(
{
}
void ImageWithCircleBackgroundLayoutElement::paint(QPainter &painter)
void ImageWithCircleBackgroundLayoutElement::paint(
QPainter &painter, const MessageColors & /*messageColors*/)
{
if (this->image_ == nullptr)
{
@ -423,7 +427,8 @@ int TextLayoutElement::getSelectionIndexCount() const
return this->getText().length() + (this->trailingSpace ? 1 : 0);
}
void TextLayoutElement::paint(QPainter &painter)
void TextLayoutElement::paint(QPainter &painter,
const MessageColors & /*messageColors*/)
{
auto app = getApp();
QString text = this->getText();
@ -532,13 +537,14 @@ int TextIconLayoutElement::getSelectionIndexCount() const
return this->trailingSpace ? 2 : 1;
}
void TextIconLayoutElement::paint(QPainter &painter)
void TextIconLayoutElement::paint(QPainter &painter,
const MessageColors &messageColors)
{
auto app = getApp();
auto *app = getApp();
QFont font = app->fonts->getFont(FontStyle::Tiny, this->scale);
painter.setPen(app->themes->messages.textColors.system);
painter.setPen(messageColors.system);
painter.setFont(font);
QTextOption option;
@ -598,7 +604,8 @@ ReplyCurveLayoutElement::ReplyCurveLayoutElement(MessageElement &creator,
{
}
void ReplyCurveLayoutElement::paint(QPainter &painter)
void ReplyCurveLayoutElement::paint(QPainter &painter,
const MessageColors & /*messageColors*/)
{
QRectF paintRect(this->getRect());
QPainterPath path;

View file

@ -21,6 +21,7 @@ class Image;
using ImagePtr = std::shared_ptr<Image>;
enum class FontStyle : uint8_t;
enum class MessageElementFlag : int64_t;
struct MessageColors;
class MessageLayoutElement : boost::noncopyable
{
@ -44,7 +45,8 @@ public:
virtual void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const = 0;
virtual int getSelectionIndexCount() const = 0;
virtual void paint(QPainter &painter) = 0;
virtual void paint(QPainter &painter,
const MessageColors &messageColors) = 0;
virtual void paintAnimated(QPainter &painter, int yOffset) = 0;
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
virtual int getXFromIndex(int index) = 0;
@ -75,7 +77,7 @@ protected:
void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
@ -94,7 +96,7 @@ protected:
void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
@ -110,7 +112,7 @@ public:
const QSize &size, QColor color);
protected:
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
private:
QColor color_;
@ -125,7 +127,7 @@ public:
int padding);
protected:
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
private:
const QColor color_;
@ -147,7 +149,7 @@ protected:
void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
@ -171,7 +173,7 @@ protected:
void addCopyTextToString(QString &str, uint32_t from = 0,
uint32_t to = UINT32_MAX) const override;
int getSelectionIndexCount() const override;
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
@ -189,7 +191,7 @@ public:
float radius, float neededMargin);
protected:
void paint(QPainter &painter) override;
void paint(QPainter &painter, const MessageColors &messageColors) override;
void paintAnimated(QPainter &painter, int yOffset) override;
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;

View file

@ -79,8 +79,8 @@ std::unique_ptr<crashpad::CrashpadClient> installCrashHandler()
// See https://chromium.googlesource.com/crashpad/crashpad/+/HEAD/handler/crashpad_handler.md
// for documentation on available options.
if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, true,
false))
if (!client->StartHandler(handlerPath, databaseDir, {}, {}, {}, {}, {},
true, false))
{
qCDebug(chatterinoApp) << "Failed to start crashpad handler";
return nullptr;

View file

@ -27,7 +27,7 @@ void IvrApi::getSubage(QString userName, QString channelName,
})
.onError([failureCallback](auto result) {
qCWarning(chatterinoIvr)
<< "Failed IVR API Call!" << result.status()
<< "Failed IVR API Call!" << result.formatError()
<< QString(result.getData());
failureCallback();
})
@ -51,7 +51,7 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList,
})
.onError([failureCallback](auto result) {
qCWarning(chatterinoIvr)
<< "Failed IVR API Call!" << result.status()
<< "Failed IVR API Call!" << result.formatError()
<< QString(result.getData());
failureCallback();
})

View file

@ -217,17 +217,20 @@ void RecentMessagesApi::loadRecentMessages(const QString &channelName,
return Success;
})
.onError([channelPtr, onError](NetworkResult result) {
.onError([channelPtr, onError](const NetworkResult &result) {
auto shared = channelPtr.lock();
if (!shared)
{
return;
}
qCDebug(chatterinoRecentMessages)
<< "Failed to load recent messages for" << shared->getName();
shared->addMessage(makeSystemMessage(
QString("Message history service unavailable (Error %1)")
.arg(result.status())));
QStringLiteral(
"Message history service unavailable (Error: %1)")
.arg(result.formatError())));
onError();
})

View file

@ -193,7 +193,7 @@ void BttvEmotes::loadEmotes()
{
if (!Settings::instance().enableBTTVGlobalEmotes)
{
this->global_.set(EMPTY_EMOTE_MAP);
this->setEmotes(EMPTY_EMOTE_MAP);
return;
}
@ -203,13 +203,18 @@ void BttvEmotes::loadEmotes()
auto emotes = this->global_.get();
auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes);
if (pair.first)
this->global_.set(
this->setEmotes(
std::make_shared<EmoteMap>(std::move(pair.second)));
return pair.first;
})
.execute();
}
void BttvEmotes::setEmotes(std::shared_ptr<const EmoteMap> emotes)
{
this->global_.set(std::move(emotes));
}
void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
const QString &channelId,
const QString &channelDisplayName,
@ -254,23 +259,17 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
else if (result.status() == NetworkResult::timedoutStatus)
{
// TODO: Auto retry in case of a timeout, with a delay
qCWarning(chatterinoBttv)
<< "Fetching BTTV emotes for channel" << channelId
<< "failed due to timeout";
shared->addMessage(makeSystemMessage(
"Failed to fetch BetterTTV channel emotes. (timed out)"));
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(chatterinoBttv)
<< "Error fetching BTTV emotes for channel" << channelId
<< ", error" << result.status();
shared->addMessage(
makeSystemMessage("Failed to fetch BetterTTV channel "
"emotes. (unknown error)"));
<< ", error" << errorString;
shared->addMessage(makeSystemMessage(
QStringLiteral("Failed to fetch BetterTTV channel "
"emotes. (Error: %1)")
.arg(errorString)));
}
})
.execute();

View file

@ -29,6 +29,7 @@ public:
std::shared_ptr<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
void setEmotes(std::shared_ptr<const EmoteMap> emotes);
static void loadChannel(std::weak_ptr<Channel> channel,
const QString &channelId,
const QString &channelDisplayName,

View file

@ -265,7 +265,7 @@ void Emojis::loadEmojiSet()
}
std::vector<boost::variant<EmotePtr, QString>> Emojis::parse(
const QString &text)
const QString &text) const
{
auto result = std::vector<boost::variant<EmotePtr, QString>>();
int lastParsedEmojiEndIndex = 0;
@ -359,7 +359,7 @@ std::vector<boost::variant<EmotePtr, QString>> Emojis::parse(
return result;
}
QString Emojis::replaceShortCodes(const QString &text)
QString Emojis::replaceShortCodes(const QString &text) const
{
QString ret(text);
auto it = this->findShortCodesRegex_.globalMatch(text);
@ -393,4 +393,14 @@ QString Emojis::replaceShortCodes(const QString &text)
return ret;
}
const EmojiMap &Emojis::getEmojis() const
{
return this->emojis;
}
const std::vector<QString> &Emojis::getShortCodes() const
{
return this->shortCodes;
}
} // namespace chatterino

View file

@ -37,16 +37,32 @@ struct EmojiData {
using EmojiMap = ConcurrentMap<QString, std::shared_ptr<EmojiData>>;
class Emojis
class IEmojis
{
public:
virtual ~IEmojis() = default;
virtual std::vector<boost::variant<EmotePtr, QString>> parse(
const QString &text) const = 0;
virtual const EmojiMap &getEmojis() const = 0;
virtual const std::vector<QString> &getShortCodes() const = 0;
virtual QString replaceShortCodes(const QString &text) const = 0;
};
class Emojis : public IEmojis
{
public:
void initialize();
void load();
std::vector<boost::variant<EmotePtr, QString>> parse(const QString &text);
std::vector<boost::variant<EmotePtr, QString>> parse(
const QString &text) const override;
EmojiMap emojis;
std::vector<QString> shortCodes;
QString replaceShortCodes(const QString &text);
QString replaceShortCodes(const QString &text) const override;
const EmojiMap &getEmojis() const override;
const std::vector<QString> &getShortCodes() const override;
private:
void loadEmojis();

View file

@ -188,7 +188,7 @@ void FfzEmotes::loadEmotes()
{
if (!Settings::instance().enableFFZGlobalEmotes)
{
this->global_.set(EMPTY_EMOTE_MAP);
this->setEmotes(EMPTY_EMOTE_MAP);
return;
}
@ -199,13 +199,18 @@ void FfzEmotes::loadEmotes()
.timeout(30000)
.onSuccess([this](auto result) -> Outcome {
auto parsedSet = parseGlobalEmotes(result.parseJson());
this->global_.set(std::make_shared<EmoteMap>(std::move(parsedSet)));
this->setEmotes(std::make_shared<EmoteMap>(std::move(parsedSet)));
return Success;
})
.execute();
}
void FfzEmotes::setEmotes(std::shared_ptr<const EmoteMap> emotes)
{
this->global_.set(std::move(emotes));
}
void FfzEmotes::loadChannel(
std::weak_ptr<Channel> channel, const QString &channelID,
std::function<void(EmoteMap &&)> emoteCallback,
@ -268,24 +273,17 @@ void FfzEmotes::loadChannel(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
}
else if (result.status() == NetworkResult::timedoutStatus)
{
// TODO: Auto retry in case of a timeout, with a delay
qCWarning(chatterinoFfzemotes)
<< "Fetching FFZ emotes for channel" << channelID
<< "failed due to timeout";
shared->addMessage(
makeSystemMessage("Failed to fetch FrankerFaceZ channel "
"emotes. (timed out)"));
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(chatterinoFfzemotes)
<< "Error fetching FFZ emotes for channel" << channelID
<< ", error" << result.status();
shared->addMessage(
makeSystemMessage("Failed to fetch FrankerFaceZ channel "
"emotes. (unknown error)"));
<< ", error" << errorString;
shared->addMessage(makeSystemMessage(
QStringLiteral("Failed to fetch FrankerFaceZ channel "
"emotes. (Error: %1)")
.arg(errorString)));
}
})
.execute();

View file

@ -22,6 +22,7 @@ public:
std::shared_ptr<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
void setEmotes(std::shared_ptr<const EmoteMap> emotes);
static void loadChannel(
std::weak_ptr<Channel> channel, const QString &channelId,
std::function<void(EmoteMap &&)> emoteCallback,

View file

@ -64,7 +64,11 @@ MessagePtr IrcMessageBuilder::build()
// message
this->addIrcMessageText(this->originalMessage_);
this->message().searchText = this->message().localizedName + " " +
QString stylizedUsername =
this->stylizeUsername(this->userName, this->message());
this->message().searchText = stylizedUsername + " " +
this->message().localizedName + " " +
this->userName + ": " + this->originalMessage_;
// highlights

View file

@ -11,9 +11,9 @@
#include "providers/twitch/TwitchIrcServer.hpp" // NOTE: Included to access the mentions channel
#include "singletons/Settings.hpp"
#include "util/IrcHelpers.hpp"
#include "util/QObjectRef.hpp"
#include <QMetaEnum>
#include <QPointer>
#include <cassert>
#include <cstdlib>
@ -151,7 +151,7 @@ void IrcServer::initializeConnection(IrcConnection *connection,
[[fallthrough]];
case IrcAuthType::Pass:
this->data_->getPassword(
this, [conn = new QObjectRef(connection) /* can't copy */,
this, [conn = new QPointer(connection) /* can't copy */,
this](const QString &password) mutable {
if (*conn)
{

View file

@ -87,8 +87,11 @@ public:
this->websocketClient_.set_fail_handler([this](auto hdl) {
this->onConnectionFail(hdl);
});
this->websocketClient_.set_user_agent("Chatterino/" CHATTERINO_VERSION
" (" CHATTERINO_GIT_HASH ")");
this->websocketClient_.set_user_agent(
QStringLiteral("Chatterino/%1 (%2)")
.arg(Version::instance().version(),
Version::instance().commitHash())
.toStdString());
}
virtual ~BasicPubSubManager() = default;

View file

@ -0,0 +1,92 @@
#include "providers/seventv/SeventvAPI.hpp"
#include "common/Literals.hpp"
#include "common/NetworkRequest.hpp"
#include "common/NetworkResult.hpp"
#include "common/Outcome.hpp"
namespace {
using namespace chatterino::literals;
const QString API_URL_USER = u"https://7tv.io/v3/users/twitch/%1"_s;
const QString API_URL_EMOTE_SET = u"https://7tv.io/v3/emote-sets/%1"_s;
const QString API_URL_PRESENCES = u"https://7tv.io/v3/users/%1/presences"_s;
} // namespace
// NOLINTBEGIN(readability-convert-member-functions-to-static)
namespace chatterino {
void SeventvAPI::getUserByTwitchID(
const QString &twitchID, SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError)
{
NetworkRequest(API_URL_USER.arg(twitchID), NetworkRequestType::Get)
.timeout(20000)
.onSuccess([callback = std::move(onSuccess)](
const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
callback(json);
return Success;
})
.onError([callback = std::move(onError)](const NetworkResult &result) {
callback(result);
})
.execute();
}
void SeventvAPI::getEmoteSet(const QString &emoteSet,
SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError)
{
NetworkRequest(API_URL_EMOTE_SET.arg(emoteSet), NetworkRequestType::Get)
.timeout(25000)
.onSuccess([callback = std::move(onSuccess)](
const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
callback(json);
return Success;
})
.onError([callback = std::move(onError)](const NetworkResult &result) {
callback(result);
})
.execute();
}
void SeventvAPI::updatePresence(const QString &twitchChannelID,
const QString &seventvUserID,
SuccessCallback<> &&onSuccess,
ErrorCallback &&onError)
{
QJsonObject payload{
{u"kind"_s, 1}, // UserPresenceKindChannel
{u"data"_s,
QJsonObject{
{u"id"_s, twitchChannelID},
{u"platform"_s, u"TWITCH"_s},
}},
};
NetworkRequest(API_URL_PRESENCES.arg(seventvUserID),
NetworkRequestType::Post)
.json(payload)
.timeout(10000)
.onSuccess([callback = std::move(onSuccess)](const auto &) -> Outcome {
callback();
return Success;
})
.onError([callback = std::move(onError)](const NetworkResult &result) {
callback(result);
})
.execute();
}
SeventvAPI &getSeventvAPI()
{
static SeventvAPI instance;
return instance;
}
} // namespace chatterino
// NOLINTEND(readability-convert-member-functions-to-static)

View file

@ -0,0 +1,33 @@
#pragma once
#include <functional>
class QString;
class QJsonObject;
namespace chatterino {
class NetworkResult;
class SeventvAPI
{
using ErrorCallback = std::function<void(const NetworkResult &)>;
template <typename... T>
using SuccessCallback = std::function<void(T...)>;
public:
void getUserByTwitchID(const QString &twitchID,
SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError);
void getEmoteSet(const QString &emoteSet,
SuccessCallback<const QJsonObject &> &&onSuccess,
ErrorCallback &&onError);
void updatePresence(const QString &twitchChannelID,
const QString &seventvUserID,
SuccessCallback<> &&onSuccess, ErrorCallback &&onError);
};
SeventvAPI &getSeventvAPI();
} // namespace chatterino

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