mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Merge branch 'master' of github.com:Chatterino/chatterino2 into chore/eradicate_hardcoded_names_from_our_amazing_codebase
This commit is contained in:
commit
d38caafcbc
222 changed files with 8851 additions and 3057 deletions
47
.CI/build-installer.ps1
Normal file
47
.CI/build-installer.ps1
Normal 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"
|
87
.CI/chatterino-installer.iss
Normal file
87
.CI/chatterino-installer.iss
Normal 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"
|
|
@ -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
14
.git-blame-ignore-revs
Normal 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
|
48
.github/workflows/build.yml
vendored
48
.github/workflows/build.yml
vendored
|
@ -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
|
||||
|
|
1
.github/workflows/check-formatting.yml
vendored
1
.github/workflows/check-formatting.yml
vendored
|
@ -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
56
.github/workflows/create-installer.yml
vendored
Normal 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
|
3
.github/workflows/lint.yml
vendored
3
.github/workflows/lint.yml
vendored
|
@ -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 .
|
||||
|
|
2
.github/workflows/post-clang-tidy-review.yml
vendored
2
.github/workflows/post-clang-tidy-review.yml
vendored
|
@ -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: ""
|
||||
|
|
38
.github/workflows/test.yml
vendored
38
.github/workflows/test.yml
vendored
|
@ -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
3
.gitignore
vendored
|
@ -121,3 +121,6 @@ resources/resources_autogenerated.qrc
|
|||
|
||||
# Leftovers from running `aqt install`
|
||||
aqtinstall.log
|
||||
|
||||
# sccache (CI)
|
||||
.sccache
|
||||
|
|
61
CHANGELOG.md
61
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
12
README.md
12
README.md
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
55
mocks/.clang-format
Normal 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
7
mocks/CMakeLists.txt
Normal 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)
|
86
mocks/include/mocks/EmptyApplication.hpp
Normal file
86
mocks/include/mocks/EmptyApplication.hpp
Normal 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
|
414
mocks/include/mocks/Helix.hpp
Normal file
414
mocks/include/mocks/Helix.hpp
Normal 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
|
BIN
resources/buttons/streamerModeEnabledDark.png
Normal file
BIN
resources/buttons/streamerModeEnabledDark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
13
resources/buttons/streamerModeEnabledDark.svg
Normal file
13
resources/buttons/streamerModeEnabledDark.svg
Normal 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 |
BIN
resources/buttons/streamerModeEnabledLight.png
Normal file
BIN
resources/buttons/streamerModeEnabledLight.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
13
resources/buttons/streamerModeEnabledLight.svg
Normal file
13
resources/buttons/streamerModeEnabledLight.svg
Normal 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 |
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
170
src/common/Literals.hpp
Normal 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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
66
src/controllers/commands/builtin/chatterino/Debugging.cpp
Normal file
66
src/controllers/commands/builtin/chatterino/Debugging.cpp
Normal 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
|
17
src/controllers/commands/builtin/chatterino/Debugging.hpp
Normal file
17
src/controllers/commands/builtin/chatterino/Debugging.hpp
Normal 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
|
112
src/controllers/commands/builtin/twitch/Shoutout.cpp
Normal file
112
src/controllers/commands/builtin/twitch/Shoutout.cpp
Normal 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
|
15
src/controllers/commands/builtin/twitch/Shoutout.hpp
Normal file
15
src/controllers/commands/builtin/twitch/Shoutout.hpp
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,10 +32,10 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
|||
{
|
||||
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()))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
190
src/controllers/twitch/LiveController.cpp
Normal file
190
src/controllers/twitch/LiveController.cpp
Normal 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
|
89
src/controllers/twitch/LiveController.hpp
Normal file
89
src/controllers/twitch/LiveController.hpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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_;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
82
src/messages/layouts/MessageLayoutContext.cpp
Normal file
82
src/messages/layouts/MessageLayoutContext.cpp
Normal 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
|
74
src/messages/layouts/MessageLayoutContext.hpp
Normal file
74
src/messages/layouts/MessageLayoutContext.hpp
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
92
src/providers/seventv/SeventvAPI.cpp
Normal file
92
src/providers/seventv/SeventvAPI.cpp
Normal 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)
|
33
src/providers/seventv/SeventvAPI.hpp
Normal file
33
src/providers/seventv/SeventvAPI.hpp
Normal 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
Loading…
Reference in a new issue