mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
feat: improve @Mm2PL's CI splitting work (#3631)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com> Co-authored-by: Paweł <zneix@zneix.eu> Co-authored-by: Leon Richardt <leon.richardt@gmail.com> Co-authored-by: pajlada <rasmus.karlsson@pajlada.com> Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com> Co-authored-by: James Upjohn <jammehcow@jammehcow.co.nz> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: qooq69 <52359859+qooq69@users.noreply.github.com> Co-authored-by: karl-police <karl-police2001@bluewin.ch> Co-authored-by: Patrick Geneva <goldbattle@users.noreply.github.com> Co-authored-by: Infinitay <Infinitay@users.noreply.github.com> Co-authored-by: Mm2PL <miau@mail.kotmisia.pl> Co-authored-by: LosFarmosCTL <80157503+LosFarmosCTL@users.noreply.github.com> Co-authored-by: ooxi <violetland@mail.ru> Co-authored-by: Edgar <Edgar@AnotherFoxGuy.com> Co-authored-by: Brian <18603393+brian6932@users.noreply.github.com> Co-authored-by: oldLucke <oldlucke@gmail.com> Co-authored-by: nerix <nero.9@hotmail.de>
This commit is contained in:
parent
953af4d254
commit
d2b89f028b
|
@ -2,10 +2,7 @@
|
|||
name: Build on Linux
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -33,84 +30,81 @@ jobs:
|
|||
path: ../Qt
|
||||
key: ubuntu-latest-QtCache-${{ matrix.qt-version }}-20210109
|
||||
|
||||
# LINUX
|
||||
- name: Install p7zip (Ubuntu)
|
||||
run: sudo apt-get update && sudo apt-get -y install p7zip-full
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
aqtversion: '==1.1.1'
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
extra: --external 7z
|
||||
version: ${{ matrix.qt-version }}
|
||||
dir: "${{ github.workspace }}/qt/"
|
||||
|
||||
- name: Install dependencies (Ubuntu)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
cmake \
|
||||
virtualenv \
|
||||
rapidjson-dev \
|
||||
libssl-dev \
|
||||
libboost-dev \
|
||||
libxcb-randr0-dev \
|
||||
libboost-system-dev \
|
||||
libboost-filesystem-dev \
|
||||
libpulse-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
build-essential \
|
||||
libgl1-mesa-dev \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-xinerama0
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
cmake \
|
||||
virtualenv \
|
||||
rapidjson-dev \
|
||||
libssl-dev \
|
||||
libboost-dev \
|
||||
libxcb-randr0-dev \
|
||||
libboost-system-dev \
|
||||
libboost-filesystem-dev \
|
||||
libpulse-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
build-essential \
|
||||
libgl1-mesa-dev \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-xinerama0
|
||||
- name: Build (Ubuntu)
|
||||
if: matrix.build-system == 'qmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
qmake PREFIX=/usr ..
|
||||
make -j8
|
||||
mkdir build
|
||||
cd build
|
||||
qmake PREFIX=/usr ..
|
||||
make -j$(nproc)
|
||||
shell: bash
|
||||
|
||||
- name: Build with CMake (Ubuntu)
|
||||
if: matrix.build-system == 'cmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=appdir/usr/ \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \
|
||||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
..
|
||||
make -j8
|
||||
mkdir build
|
||||
cd build
|
||||
cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=appdir/usr/ \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \
|
||||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
..
|
||||
make -j$(nproc)
|
||||
shell: bash
|
||||
|
||||
- name: Package - AppImage (Ubuntu)
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateAppImage.sh
|
||||
cd build
|
||||
sh ./../.CI/CreateAppImage.sh
|
||||
shell: bash
|
||||
|
||||
- name: Package - .deb (Ubuntu)
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateUbuntuDeb.sh
|
||||
cd build
|
||||
sh ./../.CI/CreateUbuntuDeb.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload artifact - AppImage (Ubuntu)
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Chatterino-x86_64-${{ matrix.qt-version }}-${{ matrix.build-system }}.AppImage
|
||||
path: build/Chatterino-x86_64.AppImage
|
||||
|
||||
- name: Upload artifact - .deb (Ubuntu)
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Chatterino-${{ matrix.qt-version }}-${{ matrix.build-system }}.deb
|
||||
path: build/Chatterino.deb
|
76
.github/workflows/build-steps-macos.yml
vendored
Normal file
76
.github/workflows/build-steps-macos.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
name: Build on macOS
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
qt-version: [5.15.2, 5.12.10]
|
||||
build-system: [qmake, cmake]
|
||||
pch: [true]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
version: ${{ matrix.qt-version }}
|
||||
dir: "${{ github.workspace }}/qt/"
|
||||
|
||||
- name: Install dependencies (MacOS)
|
||||
run: |
|
||||
brew install boost openssl rapidjson p7zip create-dmg cmake tree
|
||||
shell: bash
|
||||
|
||||
- name: Build (MacOS)
|
||||
if: matrix.build-system == 'qmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
$Qt5_DIR/bin/qmake .. DEFINES+=$dateOfBuild
|
||||
make -j$(sysctl -n hw.logicalcpu)
|
||||
shell: bash
|
||||
|
||||
- name: Build with CMake (MacOS)
|
||||
if: matrix.build-system == 'cmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl \
|
||||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
..
|
||||
make -j$(sysctl -n hw.logicalcpu)
|
||||
shell: bash
|
||||
|
||||
- name: Package (MacOS)
|
||||
run: |
|
||||
ls -la
|
||||
pwd
|
||||
ls -la build || true
|
||||
cd build
|
||||
sh ./../.CI/CreateDMG.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload artifact (MacOS)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: chatterino-osx-${{ matrix.qt-version }}-${{ matrix.build-system }}.dmg
|
||||
path: build/chatterino-osx.dmg
|
99
.github/workflows/build-steps-windows.yml
vendored
Normal file
99
.github/workflows/build-steps-windows.yml
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
name: Build on Windows
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
qt-version: [5.15.2, 5.12.10]
|
||||
build-system: [qmake, cmake]
|
||||
pch: [true]
|
||||
|
||||
steps:
|
||||
- name: Set environment variables for windows-latest
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
echo "vs_version=2022" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- 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@v2
|
||||
with:
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
version: ${{ matrix.qt-version }}
|
||||
dir: "${{ github.workspace }}/qt/"
|
||||
|
||||
- name: Cache conan packages part 1
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.txt') }}
|
||||
path: ~/.conan/
|
||||
|
||||
- name: Cache conan packages part 2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: ${{ runner.os }}-conan-root-${{ hashFiles('**/conanfile.txt') }}
|
||||
path: C:/.conan/
|
||||
|
||||
- name: Add Conan to path
|
||||
run: echo "C:\Program Files\Conan\conan\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
run: |
|
||||
choco install conan -y
|
||||
|
||||
- name: Enable Developer Command Prompt
|
||||
uses: ilammy/msvc-dev-cmd@v1.10.0
|
||||
|
||||
- name: Build (Windows)
|
||||
if: matrix.build-system == 'qmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
conan install .. -b missing
|
||||
qmake ..
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO
|
||||
windeployqt release/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/
|
||||
cp release/chatterino.exe Chatterino2/
|
||||
echo nightly > Chatterino2/modes
|
||||
7z a chatterino-windows-x86-64.zip Chatterino2/
|
||||
|
||||
- name: Build with CMake (Windows)
|
||||
if: matrix.build-system == 'cmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
conan install .. -b missing
|
||||
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DUSE_CONAN=ON ..
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO
|
||||
windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/
|
||||
cp bin/chatterino.exe Chatterino2/
|
||||
echo nightly > Chatterino2/modes
|
||||
7z a chatterino-windows-x86-64.zip Chatterino2/
|
||||
- name: Upload artifact (Windows)
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: chatterino-windows-x86-64-${{ matrix.qt-version }}-${{ matrix.build-system }}.zip
|
||||
path: build/chatterino-windows-x86-64.zip
|
||||
|
||||
- name: Clean Conan pkgs
|
||||
run: conan remove "*" -fsb
|
||||
shell: bash
|
187
.github/workflows/build.yml
vendored
187
.github/workflows/build.yml
vendored
|
@ -8,169 +8,22 @@ on:
|
|||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest]
|
||||
qt-version: [5.15.2, 5.12.10]
|
||||
build-system: [qmake, cmake]
|
||||
pch: [true]
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
qt-version: 5.12.10
|
||||
build-system: cmake
|
||||
pch: true
|
||||
include:
|
||||
- os: windows-2016
|
||||
qt-version: 5.12.10
|
||||
build-system: cmake
|
||||
pch: true
|
||||
- os: ubuntu-latest
|
||||
qt-version: 5.15.2
|
||||
build-system: cmake
|
||||
pch: false
|
||||
fail-fast: false
|
||||
build-windows:
|
||||
uses: ./.github/workflows/build-steps-windows.yml
|
||||
|
||||
steps:
|
||||
- name: Set environment variables for windows-latest
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
echo "vs_version=2019" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
build-macos:
|
||||
uses: ./.github/workflows/build-steps-macos.yml
|
||||
|
||||
- name: Set environment variables for windows-2016
|
||||
if: matrix.os == 'windows-2016'
|
||||
run: |
|
||||
echo "vs_version=2017" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v2.4.0
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ../Qt
|
||||
key: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-20210109
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v2
|
||||
with:
|
||||
aqtversion: '==1.1.1'
|
||||
cached: ${{ steps.cache-qt.outputs.cache-hit }}
|
||||
extra: --external 7z
|
||||
version: ${{ matrix.qt-version }}
|
||||
|
||||
# WINDOWS
|
||||
- name: Cache conan packages
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
key: ${{ runner.os }}-conan-${{ hashFiles('**/conanfile.txt') }}-20210412
|
||||
path: C:/.conan/
|
||||
|
||||
- name: Add Conan to path
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
run: echo "C:\Program Files\Conan\conan\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
run: |
|
||||
choco install conan -y
|
||||
|
||||
- name: Enable Developer Command Prompt
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
uses: ilammy/msvc-dev-cmd@v1.10.0
|
||||
|
||||
- name: Build (Windows)
|
||||
if: startsWith(matrix.os, 'windows') && matrix.build-system == 'qmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
conan install ..
|
||||
qmake ..
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO
|
||||
windeployqt release/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/
|
||||
cp release/chatterino.exe Chatterino2/
|
||||
echo nightly > Chatterino2/modes
|
||||
7z a chatterino-windows-x86-64.zip Chatterino2/
|
||||
|
||||
- name: Build with CMake (Windows)
|
||||
if: startsWith(matrix.os, 'windows') && matrix.build-system == 'cmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
conan install ..
|
||||
cmake -G"NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DUSE_CONAN=ON ..
|
||||
set cl=/MP
|
||||
nmake /S /NOLOGO
|
||||
windeployqt bin/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/
|
||||
cp bin/chatterino.exe Chatterino2/
|
||||
echo nightly > Chatterino2/modes
|
||||
7z a chatterino-windows-x86-64.zip Chatterino2/
|
||||
|
||||
- name: Upload artifact (Windows)
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
with:
|
||||
name: chatterino-windows-x86-64-${{ matrix.qt-version }}-${{ matrix.build-system }}.zip
|
||||
path: build/chatterino-windows-x86-64.zip
|
||||
|
||||
# MACOS
|
||||
- name: Install dependencies (MacOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: |
|
||||
brew install boost openssl rapidjson p7zip create-dmg cmake tree
|
||||
shell: bash
|
||||
|
||||
- name: Build (MacOS)
|
||||
if: startsWith(matrix.os, 'macos') && matrix.build-system == 'qmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
$Qt5_DIR/bin/qmake .. DEFINES+=$dateOfBuild
|
||||
make -j8
|
||||
shell: bash
|
||||
|
||||
- name: Build with CMake (MacOS)
|
||||
if: startsWith(matrix.os, 'macos') && matrix.build-system == 'cmake'
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DOPENSSL_ROOT_DIR=/usr/local/opt/openssl \
|
||||
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
|
||||
..
|
||||
make -j8
|
||||
shell: bash
|
||||
|
||||
- name: Package (MacOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: |
|
||||
ls -la
|
||||
pwd
|
||||
ls -la build || true
|
||||
cd build
|
||||
sh ./../.CI/CreateDMG.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload artifact (MacOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
with:
|
||||
name: chatterino-osx-${{ matrix.qt-version }}-${{ matrix.build-system }}.dmg
|
||||
path: build/chatterino-osx.dmg
|
||||
build-linux:
|
||||
uses: ./.github/workflows/build-steps-linux.yml
|
||||
|
||||
create-release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/master')
|
||||
needs:
|
||||
- build-windows
|
||||
- build-macos
|
||||
- build-linux
|
||||
|
||||
steps:
|
||||
- name: Create release
|
||||
|
@ -183,45 +36,45 @@ jobs:
|
|||
backup_tag_name: backup-nightly-build
|
||||
release_name: Nightly Release
|
||||
body: |
|
||||
Nightly Build
|
||||
Nightly Build
|
||||
prerelease: true
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-windows-x86-64-5.15.2-qmake.zip
|
||||
path: windows/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-windows-x86-64-5.15.2-cmake.zip
|
||||
path: windows-cmake/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chatterino-x86_64-5.15.2-qmake.AppImage
|
||||
path: linux/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chatterino-x86_64-5.15.2-cmake.AppImage
|
||||
path: linux-cmake/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chatterino-5.15.2-qmake.deb
|
||||
path: ubuntu/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Chatterino-5.15.2-cmake.deb
|
||||
path: ubuntu-cmake/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-osx-5.15.2-qmake.dmg
|
||||
path: macos/
|
||||
|
||||
- uses: actions/download-artifact@v2.1.0
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: chatterino-osx-5.15.2-cmake.dmg
|
||||
path: macos-cmake/
|
||||
|
@ -311,4 +164,4 @@ jobs:
|
|||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./macos-cmake/chatterino-osx.dmg
|
||||
asset_name: test-cmake-chatterino-osx.dmg
|
||||
asset_content_type: application/x-bzip2
|
||||
asset_content_type: application/x-bzip2
|
2
.github/workflows/check-formatting.yml
vendored
2
.github/workflows/check-formatting.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: apt-get update
|
||||
run: sudo apt-get update
|
||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Lint Markdown files
|
||||
uses: actionsx/prettier@v2
|
||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -13,13 +13,13 @@ jobs:
|
|||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Cache Qt
|
||||
id: cache-qt
|
||||
uses: actions/cache@v2.1.7
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ../Qt
|
||||
key: ${{ runner.os }}-QtCache-20201005
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -81,6 +81,7 @@ Thumbs.db
|
|||
.vscode
|
||||
.idea
|
||||
dependencies
|
||||
.cache
|
||||
|
||||
### CMake ###
|
||||
CMakeLists.txt.user
|
||||
|
@ -102,4 +103,4 @@ CMakeUserPresets.json
|
|||
Stamp
|
||||
tmp
|
||||
Source
|
||||
Dependencies_*
|
||||
Dependencies_*
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.12 or newer**.
|
||||
|
||||
## Ubuntu 18.04
|
||||
## Ubuntu 20.04
|
||||
|
||||
_Most likely works the same for other Debian-like distros_
|
||||
|
||||
|
@ -35,7 +35,7 @@ _Most likely works the same for other Debian-like distros_
|
|||
|
||||
### Manually
|
||||
|
||||
1. Install all of the dependencies using `sudo pacman -S qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake`
|
||||
1. Install all of the dependencies using `sudo pacman -S --needed qt5-base qt5-multimedia qt5-svg qt5-tools gst-plugins-ugly gst-plugins-good boost rapidjson pkgconf openssl cmake`
|
||||
1. Go into the project directory
|
||||
1. Create a build folder and go into it (`mkdir build && cd build`)
|
||||
1. Use one of the options below to compile it
|
||||
|
|
|
@ -33,13 +33,13 @@ Note: This installation will take about 1.5 GB of disk space.
|
|||
|
||||
### For our websocket library, we need OpenSSL 1.1
|
||||
|
||||
1. Download OpenSSL for windows, version `1.1.1m`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1m.exe)**
|
||||
1. Download OpenSSL for windows, version `1.1.1n`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1n.exe)**
|
||||
2. When prompted, install OpenSSL to `C:\local\openssl`
|
||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||
|
||||
### For Qt SSL, we need OpenSSL 1.0
|
||||
|
||||
1. Download OpenSSL for Windows, version `1.0.2u`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)**
|
||||
1. Download OpenSSL for Windows, version `1.0.2u`: **[Download](https://web.archive.org/web/20211109231823/https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)**
|
||||
2. When prompted, install it to any arbitrary empty directory.
|
||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||
4. Copy the OpenSSL 1.0 files from its `\bin` folder to `C:\local\bin` (You will need to create the folder)
|
||||
|
@ -84,7 +84,7 @@ Compiling with Breakpad support enables crash reports that can be of use for dev
|
|||
|
||||
1. Open the `chatterino.pro` file by double-clicking it, or by opening it via Qt Creator.
|
||||
2. You will be presented with a screen that is titled "Configure Project". In this screen, you should have at least one option present ready to be configured, like this:
|
||||
![Qt Create Configure Project screenshot](https://i.imgur.com/dbz45mB.png)
|
||||
![Qt Create Configure Project screenshot](https://user-images.githubusercontent.com/41973452/159462759-470e5371-671e-478e-85ca-33452ca9bea3.png)
|
||||
3. Select the profile(s) you want to build with and click "Configure Project".
|
||||
|
||||
### How to run and produce builds
|
||||
|
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -3,6 +3,7 @@
|
|||
## Unversioned
|
||||
|
||||
- Major: Added customizable shortcuts. (#2340)
|
||||
- Minor: Make animated emote playback speed match browser (Firefox and Chrome) behaviour. (#3506)
|
||||
- Minor: Added middle click split to open in browser (#3356)
|
||||
- Minor: Added new search predicate to filter for messages matching a regex (#3282)
|
||||
- Minor: Add `{channel.name}`, `{channel.id}`, `{stream.game}`, `{stream.title}`, `{my.id}`, `{my.name}` placeholders for commands (#3155)
|
||||
|
@ -42,9 +43,23 @@
|
|||
- Minor: Added autocompletion for default Twitch commands starting with the dot (e.g. `.mods` which does the same as `/mods`). (#3144)
|
||||
- Minor: Sorted usernames in `Users joined/parted` messages alphabetically. (#3421)
|
||||
- Minor: Mod list, VIP list, and Users joined/parted messages are now searchable. (#3426)
|
||||
- Minor: Add search to emote popup. (#3404)
|
||||
- Minor: Add search to emote popup. (#3404, #3527)
|
||||
- Minor: Messages can now be highlighted by subscriber or founder badges. (#3445)
|
||||
- Minor: User timeout buttons can now be triggered using hotkeys. (#3483)
|
||||
- Minor: Add workaround for multipart emoji as described in [the RFC](https://mm2pl.github.io/emoji_rfc.pdf). (#3469)
|
||||
- Minor: Added a way to open channel popup by right-clicking the avatar in a usercard. (#3486)
|
||||
- Minor: Add feedback when using the whisper command `/w` incorrectly. (#3439)
|
||||
- Minor: Add feedback when writing a non-command message in the `/whispers` split. (#3439)
|
||||
- Minor: Opening streamlink through hotkeys and/or split header menu matches `/streamlink` command and shows feedback in chat as well. (#3510)
|
||||
- Minor: Removed timestamp from AutoMod messages. (#3503)
|
||||
- Minor: Added ability to copy message ID with `Shift + Right Click`. (#3481)
|
||||
- Minor: Added /popup command to open currently focused split or supplied channel in a new window. (#3529)
|
||||
- Minor: Colorize the entire split header when focused. (#3379)
|
||||
- Minor: Added incremental search to channel search. (#3544)
|
||||
- Minor: Show right click context menu anywhere within a message's line. (#3566)
|
||||
- Minor: Make Tab Layout setting only accept predefined values (#3564)
|
||||
- Minor: Added librewolf, icecat, and waterfox incognito support. (#3588)
|
||||
- Minor: Updated to Emoji v14.0 (#3612)
|
||||
- Bugfix: Fix Split Input hotkeys not being available when input is hidden (#3362)
|
||||
- Bugfix: Fixed colored usernames sometimes not working. (#3170)
|
||||
- Bugfix: Restored ability to send duplicate `/me` messages. (#3166)
|
||||
|
@ -82,6 +97,16 @@
|
|||
- Bugfix: Removed ability to reload emotes really fast (#3450)
|
||||
- Bugfix: Re-add date of build to the "About" page on nightly versions. (#3464)
|
||||
- Bugfix: Fixed crash that would occur if the user right-clicked AutoMod badge. (#3496)
|
||||
- Bugfix: Fixed being unable to drag the user card window from certain spots. (#3508)
|
||||
- Bugfix: Fixed being unable to open a usercard from inside a usercard while "Automatically close user popup when it loses focus" was enabled. (#3518)
|
||||
- Bugfix: Usercards no longer close when the originating window (e.g. a search popup) is closed. (#3518)
|
||||
- Bugfix: Disabled /popout and /streamlink from working in non-twitch channels (e.g. /whispers) when supplied no arguments. (#3541)
|
||||
- Bugfix: Fixed automod and unban messages showing when moderation actions were disabled (#3548)
|
||||
- Bugfix: Fixed crash when rendering a highlight inside of a sub message, with sub message highlights themselves turned off. (#3556)
|
||||
- Bugfix: Don't grab the keyboard in channel picker dialog (#3575)
|
||||
- BugFix: Fixed SplitInput placeholder color. (#3606)
|
||||
- BugFix: Remove game from stream/split title when set to "nothing." (#3609)
|
||||
- BugFix: Fixed double-clicking on usernames with right/middle click causing text selection. (#3608)
|
||||
- Dev: Batch checking live status for channels with live notifications that aren't connected. (#3442)
|
||||
- Dev: Add GitHub action to test builds without precompiled headers enabled. (#3327)
|
||||
- Dev: Renamed CMake's build option `USE_SYSTEM_QT5KEYCHAIN` to `USE_SYSTEM_QTKEYCHAIN`. (#3103)
|
||||
|
@ -89,6 +114,8 @@
|
|||
- Dev: Added CMake build option `BUILD_WITH_QTKEYCHAIN` to build with or without Qt5Keychain support (On by default). (#3318)
|
||||
- Dev: Added /fakemsg command for debugging (#3448)
|
||||
- Dev: Notebook::select\* functions now take an optional `focusPage` parameter (true by default) which keeps the default behaviour of selecting the page after it has been selected. If set to false, the page is _not_ focused after being selected. (#3446)
|
||||
- Dev: Updated PubSub client to use TLS v1.2 (#3599)
|
||||
- Dev: Use system logical core count for Ubuntu/macOS GitHub actions builds. (#3602)
|
||||
|
||||
## 2.3.4
|
||||
|
||||
|
|
|
@ -231,7 +231,7 @@ When informing the user about how a command is supposed to be used, we aim to fo
|
|||
|
||||
- `Usage: /block <user>`
|
||||
- `Usage: /unblock <user>. Unblocks a user.`
|
||||
- `Usage: /streamlink <channel>`
|
||||
- `Usage: /streamlink [channel]`
|
||||
- `Usage: /usercard <user> [channel]`
|
||||
|
||||
##### Bad
|
||||
|
|
|
@ -210,7 +210,6 @@ SOURCES += \
|
|||
src/providers/IvrApi.cpp \
|
||||
src/providers/LinkResolver.cpp \
|
||||
src/providers/twitch/api/Helix.cpp \
|
||||
src/providers/twitch/api/Kraken.cpp \
|
||||
src/providers/twitch/ChannelPointReward.cpp \
|
||||
src/providers/twitch/IrcMessageHandler.cpp \
|
||||
src/providers/twitch/PubsubActions.cpp \
|
||||
|
@ -455,7 +454,6 @@ HEADERS += \
|
|||
src/providers/IvrApi.hpp \
|
||||
src/providers/LinkResolver.hpp \
|
||||
src/providers/twitch/api/Helix.hpp \
|
||||
src/providers/twitch/api/Kraken.hpp \
|
||||
src/providers/twitch/ChannelPointReward.hpp \
|
||||
src/providers/twitch/ChatterinoWebSocketppLogger.hpp \
|
||||
src/providers/twitch/EmoteValue.hpp \
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[requires]
|
||||
openssl/1.1.1k
|
||||
boost/1.76.0
|
||||
openssl/1.1.1m
|
||||
boost/1.78.0
|
||||
|
||||
[generators]
|
||||
qmake
|
||||
|
|
BIN
resources/avatars/brian6932.png
Normal file
BIN
resources/avatars/brian6932.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
resources/avatars/karlpolice.png
Normal file
BIN
resources/avatars/karlpolice.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -47,6 +47,8 @@ ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png | Contributor
|
|||
xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png | Contributor
|
||||
1xelerate | https://github.com/1xelerate | :/avatars/_1xelerate.png | Contributor
|
||||
acdvs | https://github.com/acdvs | | Contributor
|
||||
karl-police | https://github.com/karl-police | :/avatars/karlpolice.png | Contributor
|
||||
brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png | Contributor
|
||||
|
||||
# If you are a contributor add yourself above this line
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,8 +2,10 @@
|
|||
<qresource prefix="/">
|
||||
<file>avatars/_1xelerate.png</file>
|
||||
<file>avatars/alazymeme.png</file>
|
||||
<file>avatars/brian6932.png</file>
|
||||
<file>avatars/fourtf.png</file>
|
||||
<file>avatars/kararty.png</file>
|
||||
<file>avatars/karlpolice.png</file>
|
||||
<file>avatars/matthewde.jpg</file>
|
||||
<file>avatars/mm2pl.png</file>
|
||||
<file>avatars/pajlada.png</file>
|
||||
|
|
|
@ -61,7 +61,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
|
||||
, commands(&this->emplace<CommandController>())
|
||||
, notifications(&this->emplace<NotificationController>())
|
||||
, twitch2(&this->emplace<TwitchIrcServer>())
|
||||
, twitch(&this->emplace<TwitchIrcServer>())
|
||||
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
|
||||
, ffzBadges(&this->emplace<FfzBadges>())
|
||||
, logging(&this->emplace<Logging>())
|
||||
|
@ -71,9 +71,6 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
this->fonts->fontChanged.connect([this]() {
|
||||
this->windows->layoutChannelViews();
|
||||
});
|
||||
|
||||
this->twitch.server = this->twitch2;
|
||||
this->twitch.pubsub = this->twitch2->pubsub;
|
||||
}
|
||||
|
||||
void Application::initialize(Settings &settings, Paths &paths)
|
||||
|
@ -147,7 +144,7 @@ int Application::run(QApplication &qtApp)
|
|||
{
|
||||
assert(isAppInitialized);
|
||||
|
||||
this->twitch.server->connect();
|
||||
this->twitch->connect();
|
||||
|
||||
if (!getArgs().isFramelessEmbed)
|
||||
{
|
||||
|
@ -199,10 +196,9 @@ void Application::initNm(Paths &paths)
|
|||
|
||||
void Application::initPubsub()
|
||||
{
|
||||
this->twitch.pubsub->signals_.moderation.chatCleared.connect(
|
||||
this->twitch->pubsub->signals_.moderation.chatCleared.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
|
@ -217,10 +213,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.modeChanged.connect(
|
||||
this->twitch->pubsub->signals_.moderation.modeChanged.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
|
@ -244,10 +239,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.moderationStateChanged.connect(
|
||||
this->twitch->pubsub->signals_.moderation.moderationStateChanged.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
|
@ -266,10 +260,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.userBanned.connect(
|
||||
this->twitch->pubsub->signals_.moderation.userBanned.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -283,10 +276,9 @@ void Application::initPubsub()
|
|||
chan->addOrReplaceTimeout(msg);
|
||||
});
|
||||
});
|
||||
this->twitch.pubsub->signals_.moderation.messageDeleted.connect(
|
||||
this->twitch->pubsub->signals_.moderation.messageDeleted.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty() || getSettings()->hideDeletionActions)
|
||||
{
|
||||
|
@ -324,10 +316,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.userUnbanned.connect(
|
||||
this->twitch->pubsub->signals_.moderation.userUnbanned.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -341,10 +332,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.automodMessage.connect(
|
||||
this->twitch->pubsub->signals_.moderation.automodMessage.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -358,10 +348,9 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.automodUserMessage.connect(
|
||||
this->twitch->pubsub->signals_.moderation.automodUserMessage.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -376,10 +365,9 @@ void Application::initPubsub()
|
|||
chan->deleteMessage(msg->id);
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.moderation.automodInfoMessage.connect(
|
||||
this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan =
|
||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -392,36 +380,38 @@ void Application::initPubsub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) {
|
||||
QString channelId;
|
||||
if (rj::getSafe(data, "channel_id", channelId))
|
||||
{
|
||||
auto chan = this->twitch.server->getChannelOrEmptyByID(channelId);
|
||||
this->twitch->pubsub->signals_.pointReward.redeemed.connect(
|
||||
[&](auto &data) {
|
||||
QString channelId;
|
||||
if (rj::getSafe(data, "channel_id", channelId))
|
||||
{
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(channelId);
|
||||
|
||||
auto reward = ChannelPointReward(data);
|
||||
auto reward = ChannelPointReward(data);
|
||||
|
||||
postToThread([chan, reward] {
|
||||
if (auto channel = dynamic_cast<TwitchChannel *>(chan.get()))
|
||||
{
|
||||
channel->addChannelPointReward(reward);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoApp)
|
||||
<< "Couldn't find channel id of point reward";
|
||||
}
|
||||
});
|
||||
postToThread([chan, reward] {
|
||||
if (auto channel =
|
||||
dynamic_cast<TwitchChannel *>(chan.get()))
|
||||
{
|
||||
channel->addChannelPointReward(reward);
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
qCDebug(chatterinoApp)
|
||||
<< "Couldn't find channel id of point reward";
|
||||
}
|
||||
});
|
||||
|
||||
this->twitch.pubsub->start();
|
||||
this->twitch->pubsub->start();
|
||||
|
||||
auto RequestModerationActions = [=]() {
|
||||
this->twitch.server->pubsub->unlistenAllModerationActions();
|
||||
this->twitch->pubsub->unlistenAllModerationActions();
|
||||
// TODO(pajlada): Unlisten to all authed topics instead of only
|
||||
// moderation topics this->twitch.pubsub->UnlistenAllAuthedTopics();
|
||||
// moderation topics this->twitch->pubsub->UnlistenAllAuthedTopics();
|
||||
|
||||
this->twitch.server->pubsub->listenToWhispers(
|
||||
this->twitch->pubsub->listenToWhispers(
|
||||
this->accounts->twitch.getCurrent());
|
||||
};
|
||||
|
||||
|
|
|
@ -58,18 +58,12 @@ public:
|
|||
|
||||
CommandController *const commands{};
|
||||
NotificationController *const notifications{};
|
||||
TwitchIrcServer *const twitch2{};
|
||||
TwitchIrcServer *const twitch{};
|
||||
ChatterinoBadges *const chatterinoBadges{};
|
||||
FfzBadges *const ffzBadges{};
|
||||
|
||||
/*[[deprecated]]*/ Logging *const logging{};
|
||||
|
||||
/// Provider-specific
|
||||
struct {
|
||||
/*[[deprecated("use twitch2 instead")]]*/ TwitchIrcServer *server{};
|
||||
/*[[deprecated("use twitch2->pubsub instead")]]*/ PubSub *pubsub{};
|
||||
} twitch;
|
||||
|
||||
private:
|
||||
void addSingleton(Singleton *singleton);
|
||||
void initPubsub();
|
||||
|
|
|
@ -239,8 +239,6 @@ set(SOURCE_FILES
|
|||
|
||||
providers/twitch/api/Helix.cpp
|
||||
providers/twitch/api/Helix.hpp
|
||||
providers/twitch/api/Kraken.cpp
|
||||
providers/twitch/api/Kraken.hpp
|
||||
|
||||
singletons/Badges.cpp
|
||||
singletons/Badges.hpp
|
||||
|
|
|
@ -79,6 +79,8 @@ namespace {
|
|||
|
||||
QApplication::setStyle(QStyleFactory::create("Fusion"));
|
||||
|
||||
QApplication::setWindowIcon(QIcon(":/icon.ico"));
|
||||
|
||||
installCustomPalette();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ Resources2::Resources2()
|
|||
{
|
||||
this->avatars._1xelerate = QPixmap(":/avatars/_1xelerate.png");
|
||||
this->avatars.alazymeme = QPixmap(":/avatars/alazymeme.png");
|
||||
this->avatars.brian6932 = QPixmap(":/avatars/brian6932.png");
|
||||
this->avatars.fourtf = QPixmap(":/avatars/fourtf.png");
|
||||
this->avatars.kararty = QPixmap(":/avatars/kararty.png");
|
||||
this->avatars.karlpolice = QPixmap(":/avatars/karl-police.png");
|
||||
this->avatars.mm2pl = QPixmap(":/avatars/mm2pl.png");
|
||||
this->avatars.pajlada = QPixmap(":/avatars/pajlada.png");
|
||||
this->avatars.slch = QPixmap(":/avatars/slch.png");
|
||||
|
|
|
@ -11,8 +11,10 @@ public:
|
|||
struct {
|
||||
QPixmap _1xelerate;
|
||||
QPixmap alazymeme;
|
||||
QPixmap brian6932;
|
||||
QPixmap fourtf;
|
||||
QPixmap kararty;
|
||||
QPixmap karlpolice;
|
||||
QPixmap mm2pl;
|
||||
QPixmap pajlada;
|
||||
QPixmap slch;
|
||||
|
|
|
@ -142,13 +142,13 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
|
|||
}
|
||||
|
||||
// Bttv Global
|
||||
for (auto &emote : *getApp()->twitch2->getBttvEmotes().emotes())
|
||||
for (auto &emote : *getApp()->twitch->getBttvEmotes().emotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::BTTVChannelEmote);
|
||||
}
|
||||
|
||||
// Ffz Global
|
||||
for (auto &emote : *getApp()->twitch2->getFfzEmotes().emotes())
|
||||
for (auto &emote : *getApp()->twitch->getFfzEmotes().emotes())
|
||||
{
|
||||
addString(emote.first.string, TaggedString::Type::FFZChannelEmote);
|
||||
}
|
||||
|
|
|
@ -35,32 +35,6 @@
|
|||
namespace {
|
||||
using namespace chatterino;
|
||||
|
||||
// stripUserName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripUserName(QString &userName)
|
||||
{
|
||||
if (userName.startsWith('@'))
|
||||
{
|
||||
userName.remove(0, 1);
|
||||
}
|
||||
if (userName.endsWith(','))
|
||||
{
|
||||
userName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
// stripChannelName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripChannelName(QString &channelName)
|
||||
{
|
||||
if (channelName.startsWith('@') || channelName.startsWith('#'))
|
||||
{
|
||||
channelName.remove(0, 1);
|
||||
}
|
||||
if (channelName.endsWith(','))
|
||||
{
|
||||
channelName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
void sendWhisperMessage(const QString &text)
|
||||
{
|
||||
// (hemirt) pajlada: "we should not be sending whispers through jtv, but
|
||||
|
@ -74,7 +48,7 @@ void sendWhisperMessage(const QString &text)
|
|||
// Constants used here are defined in TwitchChannel.hpp
|
||||
toSend.replace(ZERO_WIDTH_JOINER, ESCAPE_TAG);
|
||||
|
||||
app->twitch.server->sendMessage("jtv", toSend);
|
||||
app->twitch->sendMessage("jtv", toSend);
|
||||
}
|
||||
|
||||
bool appendWhisperMessageWordsLocally(const QStringList &words)
|
||||
|
@ -94,8 +68,8 @@ bool appendWhisperMessageWordsLocally(const QStringList &words)
|
|||
|
||||
const auto &acc = app->accounts->twitch.getCurrent();
|
||||
const auto &accemotes = *acc->accessEmotes();
|
||||
const auto &bttvemotes = app->twitch.server->getBttvEmotes();
|
||||
const auto &ffzemotes = app->twitch.server->getFfzEmotes();
|
||||
const auto &bttvemotes = app->twitch->getBttvEmotes();
|
||||
const auto &ffzemotes = app->twitch->getFfzEmotes();
|
||||
auto flags = MessageElementFlags();
|
||||
auto emote = boost::optional<EmotePtr>{};
|
||||
for (int i = 2; i < words.length(); i++)
|
||||
|
@ -162,14 +136,14 @@ bool appendWhisperMessageWordsLocally(const QStringList &words)
|
|||
b->flags.set(MessageFlag::Whisper);
|
||||
auto messagexD = b.release();
|
||||
|
||||
app->twitch.server->whispersChannel->addMessage(messagexD);
|
||||
app->twitch->whispersChannel->addMessage(messagexD);
|
||||
|
||||
auto overrideFlags = boost::optional<MessageFlags>(messagexD->flags);
|
||||
overrideFlags->set(MessageFlag::DoNotLog);
|
||||
|
||||
if (getSettings()->inlineWhispers)
|
||||
{
|
||||
app->twitch.server->forEachChannel(
|
||||
app->twitch->forEachChannel(
|
||||
[&messagexD, overrideFlags](ChannelPtr _channel) {
|
||||
_channel->addMessage(messagexD, overrideFlags);
|
||||
});
|
||||
|
@ -527,7 +501,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
stripChannelName(channelName);
|
||||
|
||||
ChannelPtr channelTemp =
|
||||
getApp()->twitch2->getChannelOrEmpty(channelName);
|
||||
getApp()->twitch->getChannelOrEmpty(channelName);
|
||||
|
||||
if (channelTemp->isEmpty())
|
||||
{
|
||||
|
@ -655,34 +629,45 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand(
|
||||
"/streamlink", [](const QStringList &words, ChannelPtr channel) {
|
||||
QString target(words.size() < 2 ? channel->getName() : words[1]);
|
||||
this->registerCommand("/streamlink", [](const QStringList &words,
|
||||
ChannelPtr channel) {
|
||||
QString target(words.value(1));
|
||||
|
||||
if (words.size() < 2 &&
|
||||
(!channel->isTwitchChannel() || channel->isEmpty()))
|
||||
if (target.isEmpty())
|
||||
{
|
||||
if (channel->getType() == Channel::Type::Twitch &&
|
||||
!channel->isEmpty())
|
||||
{
|
||||
target = channel->getName();
|
||||
}
|
||||
else
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"Usage: /streamlink <channel>. You can also use the "
|
||||
"command without arguments in any Twitch channel to open "
|
||||
"it in streamlink."));
|
||||
"/streamlink [channel]. Open specified Twitch channel in "
|
||||
"streamlink. If no channel argument is specified, open the "
|
||||
"current Twitch channel instead."));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
stripChannelName(target);
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Opening %1 in streamlink...").arg(target)));
|
||||
openStreamlinkForChannel(target);
|
||||
stripChannelName(target);
|
||||
openStreamlinkForChannel(target);
|
||||
|
||||
return "";
|
||||
});
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand(
|
||||
"/popout", [](const QStringList &words, ChannelPtr channel) {
|
||||
QString target(words.size() < 2 ? channel->getName() : words[1]);
|
||||
this->registerCommand("/popout", [](const QStringList &words,
|
||||
ChannelPtr channel) {
|
||||
QString target(words.value(1));
|
||||
|
||||
if (words.size() < 2 &&
|
||||
(!channel->isTwitchChannel() || channel->isEmpty()))
|
||||
if (target.isEmpty())
|
||||
{
|
||||
if (channel->getType() == Channel::Type::Twitch &&
|
||||
!channel->isEmpty())
|
||||
{
|
||||
target = channel->getName();
|
||||
}
|
||||
else
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"Usage: /popout <channel>. You can also use the command "
|
||||
|
@ -690,14 +675,60 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
"popout chat."));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
stripChannelName(target);
|
||||
QDesktopServices::openUrl(
|
||||
QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=")
|
||||
.arg(target)));
|
||||
stripChannelName(target);
|
||||
QDesktopServices::openUrl(
|
||||
QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=")
|
||||
.arg(target)));
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/popup", [](const QStringList &words,
|
||||
ChannelPtr channel) {
|
||||
static const auto *usageMessage =
|
||||
"Usage: /popup [channel]. Open specified Twitch channel in "
|
||||
"a new window. If no channel argument is specified, open "
|
||||
"the currently selected split instead.";
|
||||
|
||||
QString target(words.value(1));
|
||||
stripChannelName(target);
|
||||
|
||||
if (target.isEmpty())
|
||||
{
|
||||
auto *currentPage =
|
||||
dynamic_cast<SplitContainer *>(getApp()
|
||||
->windows->getMainWindow()
|
||||
.getNotebook()
|
||||
.getSelectedPage());
|
||||
if (currentPage != nullptr)
|
||||
{
|
||||
auto *currentSplit = currentPage->getSelectedSplit();
|
||||
if (currentSplit != nullptr)
|
||||
{
|
||||
currentSplit->popup();
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
channel->addMessage(makeSystemMessage(usageMessage));
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
auto *app = getApp();
|
||||
Window &window = app->windows->createWindow(WindowType::Popup, true);
|
||||
|
||||
auto *split = new Split(static_cast<SplitContainer *>(
|
||||
window.getNotebook().getOrAddSelectedPage()));
|
||||
|
||||
split->setChannel(app->twitch->getOrAddChannel(target));
|
||||
|
||||
window.getNotebook().getOrAddSelectedPage()->appendSplit(split);
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/clearmessages", [](const auto & /*words*/,
|
||||
ChannelPtr channel) {
|
||||
|
@ -876,7 +907,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
});
|
||||
|
||||
this->registerCommand("/raw", [](const QStringList &words, ChannelPtr) {
|
||||
getApp()->twitch2->sendRawMessage(words.mid(1).join(" "));
|
||||
getApp()->twitch->sendRawMessage(words.mid(1).join(" "));
|
||||
return "";
|
||||
});
|
||||
#ifndef NDEBUG
|
||||
|
@ -891,7 +922,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
return "";
|
||||
}
|
||||
auto ircText = words.mid(1).join(" ");
|
||||
getApp()->twitch2->addFakeMessage(ircText);
|
||||
getApp()->twitch->addFakeMessage(ircText);
|
||||
return "";
|
||||
});
|
||||
#endif
|
||||
|
@ -933,6 +964,11 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
appendWhisperMessageWordsLocally(words);
|
||||
sendWhisperMessage(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("Usage: /w <username> <message>"));
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
@ -980,6 +1016,13 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
|||
}
|
||||
}
|
||||
|
||||
if (!dryRun && channel->getType() == Channel::Type::TwitchWhispers)
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("Use /w <username> <message> to whisper"));
|
||||
return "";
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,7 @@ namespace filterparser {
|
|||
|
||||
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||
{
|
||||
auto watchingChannel =
|
||||
chatterino::getApp()->twitch.server->watchingChannel.get();
|
||||
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();
|
||||
|
||||
/* Known Identifiers
|
||||
*
|
||||
|
|
|
@ -44,6 +44,10 @@ inline const std::map<HotkeyCategory, ActionDefinitionMap> actionNames{
|
|||
1,
|
||||
}},
|
||||
{"search", ActionDefinition{"Focus search box"}},
|
||||
{"execModeratorAction",
|
||||
ActionDefinition{
|
||||
"Usercard: execute moderation action",
|
||||
"<ban, unban or number of the timeout button to use>", 1}},
|
||||
}},
|
||||
{HotkeyCategory::Split,
|
||||
{
|
||||
|
|
|
@ -163,7 +163,7 @@ void NotificationController::fetchFakeChannels()
|
|||
for (std::vector<int>::size_type i = 0;
|
||||
i != channelMap[Platform::Twitch].raw().size(); i++)
|
||||
{
|
||||
auto chan = getApp()->twitch.server->getChannelOrEmpty(
|
||||
auto chan = getApp()->twitch->getChannelOrEmpty(
|
||||
channelMap[Platform::Twitch].raw()[i]);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -236,7 +236,7 @@ void NotificationController::checkStream(bool live, QString channelName)
|
|||
}
|
||||
MessageBuilder builder;
|
||||
TwitchMessageBuilder::liveMessage(channelName, &builder);
|
||||
getApp()->twitch2->liveChannel->addMessage(builder.release());
|
||||
getApp()->twitch->liveChannel->addMessage(builder.release());
|
||||
|
||||
// Indicate that we have pushed notifications for this stream
|
||||
fakeTwitchChannels.push_back(channelName);
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
#include "common/Version.hpp"
|
||||
#include "providers/IvrApi.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/AttachToConsole.hpp"
|
||||
|
@ -83,7 +82,6 @@ int main(int argc, char **argv)
|
|||
|
||||
IvrApi::initialize();
|
||||
Helix::initialize();
|
||||
Kraken::initialize();
|
||||
|
||||
Settings settings(paths->settingsDirectory);
|
||||
|
||||
|
|
|
@ -152,8 +152,15 @@ namespace detail {
|
|||
if (reader.read(&image))
|
||||
{
|
||||
QPixmap::fromImage(image);
|
||||
|
||||
int duration = std::max(20, reader.nextImageDelay());
|
||||
// It seems that browsers have special logic for fast animations.
|
||||
// This implements Chrome and Firefox's behavior which uses
|
||||
// a duration of 100 ms for any frames that specify a duration of <= 10 ms.
|
||||
// See http://webkit.org/b/36082 for more information.
|
||||
// https://github.com/SevenTV/chatterino7/issues/46#issuecomment-1010595231
|
||||
int duration = reader.nextImageDelay();
|
||||
if (duration <= 10)
|
||||
duration = 100;
|
||||
duration = std::max(20, duration);
|
||||
frames.push_back(Frame<QImage>{image, duration});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,9 +96,10 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|||
|
||||
//
|
||||
// Builder for AutoMod message with explanation
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.message().loginName = "automod";
|
||||
builder.message().flags.set(MessageFlag::PubSub);
|
||||
builder.message().flags.set(MessageFlag::Timeout);
|
||||
builder.message().flags.set(MessageFlag::AutoMod);
|
||||
|
||||
// AutoMod shield badge
|
||||
builder.emplace<BadgeElement>(makeAutoModBadge(),
|
||||
|
@ -130,7 +131,6 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|||
// ID of message caught by AutoMod
|
||||
// builder.emplace<TextElement>(action.msgID, MessageElementFlag::Text,
|
||||
// MessageColor::Text);
|
||||
builder.message().flags.set(MessageFlag::AutoMod);
|
||||
auto text1 =
|
||||
QString("AutoMod: Held a message for reason: %1. Allow will post "
|
||||
"it in chat. Allow Deny")
|
||||
|
@ -146,6 +146,8 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|||
builder2.emplace<TwitchModerationElement>();
|
||||
builder2.message().loginName = action.target.login;
|
||||
builder2.message().flags.set(MessageFlag::PubSub);
|
||||
builder2.message().flags.set(MessageFlag::Timeout);
|
||||
builder2.message().flags.set(MessageFlag::AutoMod);
|
||||
|
||||
// sender username
|
||||
builder2
|
||||
|
@ -161,7 +163,6 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|||
// sender's message caught by AutoMod
|
||||
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
builder2.message().flags.set(MessageFlag::AutoMod);
|
||||
auto text2 =
|
||||
QString("%1: %2").arg(action.target.displayName, action.message);
|
||||
builder2.message().messageText = text2;
|
||||
|
|
|
@ -217,7 +217,8 @@ void SharedMessageBuilder::parseHighlights()
|
|||
<< "sent a message";
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||
if (!(this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight))
|
||||
{
|
||||
this->message().highlightColor = userHighlight.getColor();
|
||||
}
|
||||
|
@ -289,7 +290,8 @@ void SharedMessageBuilder::parseHighlights()
|
|||
}
|
||||
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||
if (!(this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight))
|
||||
{
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
}
|
||||
|
@ -347,7 +349,8 @@ void SharedMessageBuilder::parseHighlights()
|
|||
if (!badgeHighlightSet)
|
||||
{
|
||||
this->message().flags.set(MessageFlag::Highlighted);
|
||||
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||
if (!(this->message().flags.has(MessageFlag::Subscription) &&
|
||||
getSettings()->enableSubHighlight))
|
||||
{
|
||||
this->message().highlightColor = highlight.getColor();
|
||||
}
|
||||
|
|
|
@ -135,7 +135,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
|
|||
}
|
||||
|
||||
if (getSettings()->hideModerationActions &&
|
||||
this->message_->flags.has(MessageFlag::Timeout))
|
||||
(this->message_->flags.has(MessageFlag::Timeout) ||
|
||||
this->message_->flags.has(MessageFlag::Untimeout)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -35,8 +35,7 @@ void IvrApi::getSubage(QString userName, QString channelName,
|
|||
|
||||
void IvrApi::getBulkEmoteSets(QString emoteSetList,
|
||||
ResultCallback<QJsonArray> successCallback,
|
||||
IvrFailureCallback failureCallback,
|
||||
std::function<void()> finallyCallback)
|
||||
IvrFailureCallback failureCallback)
|
||||
{
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("set_id", emoteSetList);
|
||||
|
@ -55,7 +54,6 @@ void IvrApi::getBulkEmoteSets(QString emoteSetList,
|
|||
<< QString(result.getData());
|
||||
failureCallback();
|
||||
})
|
||||
.finally(std::move(finallyCallback))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
|
|
@ -84,8 +84,7 @@ public:
|
|||
// https://api.ivr.fi/v2/docs/static/index.html#/Twitch/get_twitch_emotes_sets
|
||||
void getBulkEmoteSets(QString emoteSetList,
|
||||
ResultCallback<QJsonArray> successCallback,
|
||||
IvrFailureCallback failureCallback,
|
||||
std::function<void()> finallyCallback);
|
||||
IvrFailureCallback failureCallback);
|
||||
|
||||
static void initialize();
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ void Emojis::load()
|
|||
|
||||
void Emojis::loadEmojis()
|
||||
{
|
||||
// Current version: https://github.com/iamcal/emoji-data/blob/v7.0.2/emoji.json (Emoji version 13.1 (2021))
|
||||
// Current version: https://github.com/iamcal/emoji-data/blob/v14.0.0/emoji.json (Emoji version 14.0 (2022))
|
||||
QFile file(":/emoji.json");
|
||||
file.open(QFile::ReadOnly);
|
||||
QTextStream s1(&file);
|
||||
|
|
|
@ -195,7 +195,7 @@ void IrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
|
|||
|
||||
if (highlighted && showInMentions)
|
||||
{
|
||||
getApp()->twitch2->mentionsChannel->addMessage(msg);
|
||||
getApp()->twitch->mentionsChannel->addMessage(msg);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -343,7 +343,7 @@ void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message)
|
|||
{
|
||||
return;
|
||||
}
|
||||
auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
|
||||
auto chan = getApp()->twitch->getChannelOrEmpty(chanName);
|
||||
|
||||
auto *twitchChannel = dynamic_cast<TwitchChannel *>(chan.get());
|
||||
if (!twitchChannel)
|
||||
|
@ -404,7 +404,7 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
|
|||
}
|
||||
|
||||
// get channel
|
||||
auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
|
||||
auto chan = getApp()->twitch->getChannelOrEmpty(chanName);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -463,7 +463,7 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
|
|||
}
|
||||
|
||||
// get channel
|
||||
auto chan = getApp()->twitch.server->getChannelOrEmpty(chanName);
|
||||
auto chan = getApp()->twitch->getChannelOrEmpty(chanName);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
|
@ -501,7 +501,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
|||
|
||||
if (emoteSetsChanged)
|
||||
{
|
||||
currentUser->loadUserstateEmotes([] {});
|
||||
currentUser->loadUserstateEmotes();
|
||||
}
|
||||
|
||||
QString channelName;
|
||||
|
@ -510,7 +510,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
|
|||
return;
|
||||
}
|
||||
|
||||
auto c = getApp()->twitch.server->getChannelOrEmpty(channelName);
|
||||
auto c = getApp()->twitch->getChannelOrEmpty(channelName);
|
||||
if (c->isEmpty())
|
||||
{
|
||||
return;
|
||||
|
@ -565,7 +565,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
|
|||
|
||||
args.isReceivedWhisper = true;
|
||||
|
||||
auto c = getApp()->twitch.server->whispersChannel.get();
|
||||
auto c = getApp()->twitch->whispersChannel.get();
|
||||
|
||||
TwitchMessageBuilder builder(
|
||||
c, message, args,
|
||||
|
@ -581,11 +581,11 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
|
|||
MessagePtr _message = builder.build();
|
||||
builder.triggerHighlights();
|
||||
|
||||
getApp()->twitch.server->lastUserThatWhisperedMe.set(builder.userName);
|
||||
getApp()->twitch->lastUserThatWhisperedMe.set(builder.userName);
|
||||
|
||||
if (_message->flags.has(MessageFlag::Highlighted))
|
||||
{
|
||||
getApp()->twitch.server->mentionsChannel->addMessage(_message);
|
||||
getApp()->twitch->mentionsChannel->addMessage(_message);
|
||||
}
|
||||
|
||||
c->addMessage(_message);
|
||||
|
@ -596,7 +596,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
|
|||
|
||||
if (getSettings()->inlineWhispers)
|
||||
{
|
||||
getApp()->twitch.server->forEachChannel(
|
||||
getApp()->twitch->forEachChannel(
|
||||
[&_message, overrideFlags](ChannelPtr channel) {
|
||||
channel->addMessage(_message, overrideFlags);
|
||||
});
|
||||
|
@ -797,7 +797,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
|
|||
{
|
||||
// Notice wasn't targeted at a single channel, send to all twitch
|
||||
// channels
|
||||
getApp()->twitch.server->forEachChannelAndSpecialChannels(
|
||||
getApp()->twitch->forEachChannelAndSpecialChannels(
|
||||
[msg](const auto &c) {
|
||||
c->addMessage(msg);
|
||||
});
|
||||
|
@ -805,7 +805,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
|
|||
return;
|
||||
}
|
||||
|
||||
auto channel = getApp()->twitch.server->getChannelOrEmpty(channelName);
|
||||
auto channel = getApp()->twitch->getChannelOrEmpty(channelName);
|
||||
|
||||
if (channel->isEmpty())
|
||||
{
|
||||
|
@ -887,8 +887,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
|
|||
|
||||
void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
auto channel = getApp()->twitch.server->getChannelOrEmpty(
|
||||
message->parameter(0).remove(0, 1));
|
||||
auto channel =
|
||||
getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1));
|
||||
|
||||
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
||||
if (!twitchChannel)
|
||||
|
@ -906,8 +906,8 @@ void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
|
|||
|
||||
void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message)
|
||||
{
|
||||
auto channel = getApp()->twitch.server->getChannelOrEmpty(
|
||||
message->parameter(0).remove(0, 1));
|
||||
auto channel =
|
||||
getApp()->twitch->getChannelOrEmpty(message->parameter(0).remove(0, 1));
|
||||
|
||||
auto *twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
|
||||
if (!twitchChannel)
|
||||
|
|
|
@ -1178,7 +1178,7 @@ void PubSub::onConnectionClose(WebsocketHandle hdl)
|
|||
PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl)
|
||||
{
|
||||
WebsocketContextPtr ctx(
|
||||
new boost::asio::ssl::context(boost::asio::ssl::context::tlsv1));
|
||||
new boost::asio::ssl::context(boost::asio::ssl::context::tlsv12));
|
||||
|
||||
try
|
||||
{
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchUser.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
@ -217,18 +216,7 @@ void TwitchAccount::loadEmotes(std::weak_ptr<Channel> weakChannel)
|
|||
qCDebug(chatterinoTwitch) << "Cleared emotes!";
|
||||
}
|
||||
|
||||
// TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
|
||||
// For now, this is necessary as Kraken's equivalent doesn't return all emotes
|
||||
// See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
|
||||
this->loadUserstateEmotes([this, weakChannel] {
|
||||
// Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
|
||||
this->loadKrakenEmotes();
|
||||
if (auto channel = weakChannel.lock(); channel != nullptr)
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("Twitch subscriber emotes reloaded."));
|
||||
}
|
||||
});
|
||||
this->loadUserstateEmotes();
|
||||
}
|
||||
|
||||
bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
|
||||
|
@ -246,15 +234,14 @@ bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
|
|||
return true;
|
||||
}
|
||||
|
||||
void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
|
||||
void TwitchAccount::loadUserstateEmotes()
|
||||
{
|
||||
if (this->userstateEmoteSets_.isEmpty())
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList newEmoteSetKeys, krakenEmoteSetKeys;
|
||||
QStringList newEmoteSetKeys, existingEmoteSetKeys;
|
||||
|
||||
auto emoteData = this->emotes_.access();
|
||||
auto userEmoteSets = emoteData->emoteSets;
|
||||
|
@ -262,13 +249,13 @@ void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
|
|||
// get list of already fetched emote sets
|
||||
for (const auto &userEmoteSet : userEmoteSets)
|
||||
{
|
||||
krakenEmoteSetKeys.push_back(userEmoteSet->key);
|
||||
existingEmoteSetKeys.push_back(userEmoteSet->key);
|
||||
}
|
||||
|
||||
// filter out emote sets from userstate message, which are not in fetched emote set list
|
||||
for (const auto &emoteSetKey : qAsConst(this->userstateEmoteSets_))
|
||||
{
|
||||
if (!krakenEmoteSetKeys.contains(emoteSetKey))
|
||||
if (!existingEmoteSetKeys.contains(emoteSetKey))
|
||||
{
|
||||
newEmoteSetKeys.push_back(emoteSetKey);
|
||||
}
|
||||
|
@ -277,7 +264,6 @@ void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
|
|||
// return if there are no new emote sets
|
||||
if (newEmoteSetKeys.isEmpty())
|
||||
{
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -365,16 +351,6 @@ void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
|
|||
},
|
||||
[] {
|
||||
// fetching emotes failed, ivr API might be down
|
||||
},
|
||||
[=] {
|
||||
// XXX(zneix): We check if this is the last iteration and if so, call the callback
|
||||
if (i + 1 == batches.size())
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Finished loading emotes from IVR, attempting to "
|
||||
"load Kraken emotes now";
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -484,79 +460,6 @@ void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel)
|
|||
});
|
||||
}
|
||||
|
||||
void TwitchAccount::loadKrakenEmotes()
|
||||
{
|
||||
getKraken()->getUserEmotes(
|
||||
this,
|
||||
[this](KrakenEmoteSets data) {
|
||||
// no emotes available
|
||||
if (data.emoteSets.isEmpty())
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "\"emoticon_sets\" either empty or not present in "
|
||||
"Kraken::getUserEmotes response";
|
||||
return;
|
||||
}
|
||||
|
||||
auto emoteData = this->emotes_.access();
|
||||
|
||||
for (auto emoteSetIt = data.emoteSets.begin();
|
||||
emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
|
||||
{
|
||||
auto emoteSet = std::make_shared<EmoteSet>();
|
||||
|
||||
QString setKey = emoteSetIt.key();
|
||||
emoteSet->key = setKey;
|
||||
this->loadEmoteSetData(emoteSet);
|
||||
|
||||
// check if the emoteset is already in emoteData
|
||||
auto isAlreadyFetched = std::find_if(
|
||||
emoteData->emoteSets.begin(), emoteData->emoteSets.end(),
|
||||
[setKey](std::shared_ptr<EmoteSet> set) {
|
||||
return (set->key == setKey);
|
||||
});
|
||||
if (isAlreadyFetched != emoteData->emoteSets.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto emoteArrObj : emoteSetIt->toArray())
|
||||
{
|
||||
if (!emoteArrObj.isObject())
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< QString("Emote value from set %1 was invalid")
|
||||
.arg(emoteSet->key);
|
||||
continue;
|
||||
}
|
||||
KrakenEmote krakenEmote(emoteArrObj.toObject());
|
||||
|
||||
auto id = EmoteId{krakenEmote.id};
|
||||
auto code = EmoteName{
|
||||
TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
|
||||
|
||||
emoteSet->emotes.emplace_back(TwitchEmote{id, code});
|
||||
|
||||
if (!emoteSet->local)
|
||||
{
|
||||
auto emote =
|
||||
getApp()->emotes->twitch.getOrCreateEmote(id, code);
|
||||
emoteData->emotes.emplace(code, emote);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
|
||||
[](const TwitchEmote &l, const TwitchEmote &r) {
|
||||
return l.name.string < r.name.string;
|
||||
});
|
||||
emoteData->emoteSets.emplace_back(emoteSet);
|
||||
}
|
||||
},
|
||||
[] {
|
||||
// kraken request failed
|
||||
});
|
||||
}
|
||||
|
||||
void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
|
||||
{
|
||||
if (!emoteSet)
|
||||
|
|
|
@ -114,7 +114,7 @@ public:
|
|||
void loadEmotes(std::weak_ptr<Channel> weakChannel = {});
|
||||
// loadUserstateEmotes loads emote sets that are part of the USERSTATE emote-sets key
|
||||
// this function makes sure not to load emote sets that have already been loaded
|
||||
void loadUserstateEmotes(std::function<void()> callback);
|
||||
void loadUserstateEmotes();
|
||||
// setUserStateEmoteSets sets the emote sets that were parsed from the USERSTATE emote-sets key
|
||||
// Returns true if the newly inserted emote sets differ from the ones previously saved
|
||||
[[nodiscard]] bool setUserstateEmoteSets(QStringList newEmoteSets);
|
||||
|
@ -127,7 +127,6 @@ public:
|
|||
void autoModDeny(const QString msgID, ChannelPtr channel);
|
||||
|
||||
private:
|
||||
void loadKrakenEmotes();
|
||||
void loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet);
|
||||
|
||||
QString oauthClient_;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -149,7 +148,6 @@ void TwitchAccountManager::load()
|
|||
qCDebug(chatterinoTwitch)
|
||||
<< "Twitch user updated to" << newUsername;
|
||||
getHelix()->update(user->getOAuthClient(), user->getOAuthToken());
|
||||
getKraken()->update(user->getOAuthClient(), user->getOAuthToken());
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Toasts.hpp"
|
||||
|
@ -483,7 +482,7 @@ bool TwitchChannel::canReconnect() const
|
|||
|
||||
void TwitchChannel::reconnect()
|
||||
{
|
||||
getApp()->twitch.server->connect();
|
||||
getApp()->twitch->connect();
|
||||
}
|
||||
|
||||
QString TwitchChannel::roomId() const
|
||||
|
@ -612,7 +611,7 @@ void TwitchChannel::setLive(bool newLiveStatus)
|
|||
MessageBuilder builder2;
|
||||
TwitchMessageBuilder::liveMessage(this->getDisplayName(),
|
||||
&builder2);
|
||||
getApp()->twitch2->liveChannel->addMessage(builder2.release());
|
||||
getApp()->twitch->liveChannel->addMessage(builder2.release());
|
||||
|
||||
// Notify on all channels with a ping sound
|
||||
if (getSettings()->notificationOnAnyChannel &&
|
||||
|
@ -632,7 +631,7 @@ void TwitchChannel::setLive(bool newLiveStatus)
|
|||
|
||||
// "delete" old 'CHANNEL is live' message
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
getApp()->twitch2->liveChannel->getMessageSnapshot();
|
||||
getApp()->twitch->liveChannel->getMessageSnapshot();
|
||||
int snapshotLength = snapshot.size();
|
||||
|
||||
// MSVC hates this code if the parens are not there
|
||||
|
@ -740,26 +739,34 @@ void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
|
|||
{
|
||||
status->gameId = stream.gameId;
|
||||
|
||||
// Resolve game ID to game name
|
||||
getHelix()->getGameById(
|
||||
stream.gameId,
|
||||
[this, weak = weakOf<Channel>(this)](const auto &game) {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!stream.gameId.isEmpty())
|
||||
{
|
||||
// Resolve game ID to game name
|
||||
getHelix()->getGameById(
|
||||
stream.gameId,
|
||||
[this, weak = weakOf<Channel>(this)](const auto &game) {
|
||||
ChannelPtr shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
status->game = game.name;
|
||||
}
|
||||
{
|
||||
auto status = this->streamStatus_.access();
|
||||
status->game = game.name;
|
||||
}
|
||||
|
||||
this->liveStatusChanged.invoke();
|
||||
},
|
||||
[] {
|
||||
// failure
|
||||
});
|
||||
this->liveStatusChanged.invoke();
|
||||
},
|
||||
[] {
|
||||
// failure
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Game is nothing and can't be resolved by the API, force empty
|
||||
status->game = "";
|
||||
}
|
||||
}
|
||||
status->title = stream.title;
|
||||
QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate);
|
||||
|
@ -878,10 +885,9 @@ void TwitchChannel::refreshPubsub()
|
|||
return;
|
||||
|
||||
auto account = getApp()->accounts->twitch.getCurrent();
|
||||
getApp()->twitch2->pubsub->listenToChannelModerationActions(roomId,
|
||||
account);
|
||||
getApp()->twitch2->pubsub->listenToAutomod(roomId, account);
|
||||
getApp()->twitch2->pubsub->listenToChannelPointRewards(roomId, account);
|
||||
getApp()->twitch->pubsub->listenToChannelModerationActions(roomId, account);
|
||||
getApp()->twitch->pubsub->listenToAutomod(roomId, account);
|
||||
getApp()->twitch->pubsub->listenToChannelPointRewards(roomId, account);
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshChatters()
|
||||
|
|
|
@ -972,8 +972,8 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
|
|||
{
|
||||
auto *app = getApp();
|
||||
|
||||
const auto &globalBttvEmotes = app->twitch.server->getBttvEmotes();
|
||||
const auto &globalFfzEmotes = app->twitch.server->getFfzEmotes();
|
||||
const auto &globalBttvEmotes = app->twitch->getBttvEmotes();
|
||||
const auto &globalFfzEmotes = app->twitch->getFfzEmotes();
|
||||
|
||||
auto flags = MessageElementFlags();
|
||||
auto emote = boost::optional<EmotePtr>{};
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
#include "providers/twitch/api/Kraken.hpp"
|
||||
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
static Kraken *instance = nullptr;
|
||||
|
||||
void Kraken::getUserEmotes(TwitchAccount *account,
|
||||
ResultCallback<KrakenEmoteSets> successCallback,
|
||||
KrakenFailureCallback failureCallback)
|
||||
{
|
||||
this->makeRequest(QString("users/%1/emotes").arg(account->getUserId()), {})
|
||||
.authorizeTwitchV5(account->getOAuthClient(), account->getOAuthToken())
|
||||
.onSuccess([successCallback](auto result) -> Outcome {
|
||||
auto data = result.parseJson();
|
||||
|
||||
KrakenEmoteSets emoteSets(data);
|
||||
|
||||
successCallback(emoteSets);
|
||||
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](NetworkResult /*result*/) {
|
||||
// TODO: make better xd
|
||||
failureCallback();
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
NetworkRequest Kraken::makeRequest(QString url, QUrlQuery urlQuery)
|
||||
{
|
||||
assert(!url.startsWith("/"));
|
||||
|
||||
if (this->clientId.isEmpty())
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Kraken::makeRequest called without a client ID set BabyRage";
|
||||
}
|
||||
|
||||
const QString baseUrl("https://api.twitch.tv/kraken/");
|
||||
|
||||
QUrl fullUrl(baseUrl + url);
|
||||
|
||||
fullUrl.setQuery(urlQuery);
|
||||
|
||||
if (!this->oauthToken.isEmpty())
|
||||
{
|
||||
return NetworkRequest(fullUrl)
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/vnd.twitchtv.v5+json")
|
||||
.header("Client-ID", this->clientId)
|
||||
.header("Authorization", "OAuth " + this->oauthToken);
|
||||
}
|
||||
|
||||
return NetworkRequest(fullUrl)
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/vnd.twitchtv.v5+json")
|
||||
.header("Client-ID", this->clientId);
|
||||
}
|
||||
|
||||
void Kraken::update(QString clientId, QString oauthToken)
|
||||
{
|
||||
this->clientId = std::move(clientId);
|
||||
this->oauthToken = std::move(oauthToken);
|
||||
}
|
||||
|
||||
void Kraken::initialize()
|
||||
{
|
||||
assert(instance == nullptr);
|
||||
|
||||
instance = new Kraken();
|
||||
|
||||
getKraken()->update(getDefaultClientID(), "");
|
||||
}
|
||||
|
||||
Kraken *getKraken()
|
||||
{
|
||||
assert(instance != nullptr);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,59 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/NetworkRequest.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using KrakenFailureCallback = std::function<void()>;
|
||||
template <typename... T>
|
||||
using ResultCallback = std::function<void(T...)>;
|
||||
|
||||
struct KrakenEmoteSets {
|
||||
const QJsonObject emoteSets;
|
||||
|
||||
KrakenEmoteSets(QJsonObject jsonObject)
|
||||
: emoteSets(jsonObject.value("emoticon_sets").toObject())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct KrakenEmote {
|
||||
const QString code;
|
||||
const QString id;
|
||||
|
||||
KrakenEmote(QJsonObject jsonObject)
|
||||
: code(jsonObject.value("code").toString())
|
||||
, id(QString::number(jsonObject.value("id").toInt()))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
class Kraken final : boost::noncopyable
|
||||
{
|
||||
public:
|
||||
// https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes
|
||||
void getUserEmotes(TwitchAccount *account,
|
||||
ResultCallback<KrakenEmoteSets> successCallback,
|
||||
KrakenFailureCallback failureCallback);
|
||||
|
||||
void update(QString clientId, QString oauthToken);
|
||||
|
||||
static void initialize();
|
||||
|
||||
private:
|
||||
NetworkRequest makeRequest(QString url, QUrlQuery urlQuery);
|
||||
|
||||
QString clientId;
|
||||
QString oauthToken;
|
||||
};
|
||||
|
||||
Kraken *getKraken();
|
||||
|
||||
} // namespace chatterino
|
|
@ -2,19 +2,6 @@
|
|||
|
||||
this folder describes what sort of API requests we do, what permissions are required for the requests etc
|
||||
|
||||
## Kraken (V5)
|
||||
|
||||
We use few Kraken endpoints in Chatterino2.
|
||||
|
||||
### Get User Emotes
|
||||
|
||||
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes
|
||||
Requires `user_subscriptions` scope
|
||||
|
||||
Migration path: **Unknown**
|
||||
|
||||
- We use this in `providers/twitch/TwitchAccount.cpp loadEmotes` to figure out which emotes a user is allowed to use!
|
||||
|
||||
## Helix
|
||||
|
||||
Full Helix API reference: https://dev.twitch.tv/docs/api/reference
|
||||
|
@ -23,134 +10,131 @@ Full Helix API reference: https://dev.twitch.tv/docs/api/reference
|
|||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-users
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp fetchUsers`.
|
||||
Used in:
|
||||
- `UserInfoPopup` to get ID, viewCount, displayName, createdAt of username we clicked
|
||||
- `CommandController` to power any commands that need to get a user ID
|
||||
- `Toasts` to get the profile picture of a streamer who just went live
|
||||
- `TwitchAccount` block and unblock features to translate user name to user ID
|
||||
Used in:
|
||||
|
||||
- `UserInfoPopup` to get ID, viewCount, displayName, createdAt of username we clicked
|
||||
- `CommandController` to power any commands that need to get a user ID
|
||||
- `Toasts` to get the profile picture of a streamer who just went live
|
||||
- `TwitchAccount` block and unblock features to translate user name to user ID
|
||||
|
||||
### Get Users Follows
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-users-follows
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp fetchUsersFollows`
|
||||
Used in:
|
||||
- `UserInfoPopup` to get number of followers a user has
|
||||
Used in:
|
||||
|
||||
- `UserInfoPopup` to get number of followers a user has
|
||||
|
||||
### Get Streams
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-streams
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp fetchStreams`
|
||||
Used in:
|
||||
- `TwitchChannel` to get live status, game, title, and viewer count of a channel
|
||||
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
|
||||
Used in:
|
||||
|
||||
- `TwitchChannel` to get live status, game, title, and viewer count of a channel
|
||||
- `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
|
||||
|
||||
### Create Clip
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#create-clip
|
||||
Requires `clips:edit` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp createClip`
|
||||
Used in:
|
||||
- `TwitchChannel` to create a clip of a live broadcast
|
||||
Used in:
|
||||
|
||||
- `TwitchChannel` to create a clip of a live broadcast
|
||||
|
||||
### Get Channel
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-channel-information
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp getChannel`
|
||||
Used in:
|
||||
- `TwitchChannel` to refresh stream title
|
||||
Used in:
|
||||
|
||||
- `TwitchChannel` to refresh stream title
|
||||
|
||||
### Update Channel
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#modify-channel-information
|
||||
Requires `channel:manage:broadcast` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp updateChannel`
|
||||
Used in:
|
||||
- `/setgame` to update the game in the current channel
|
||||
- `/settitle` to update the title in the current channel
|
||||
Used in:
|
||||
|
||||
- `/setgame` to update the game in the current channel
|
||||
- `/settitle` to update the title in the current channel
|
||||
|
||||
### Create Stream Marker
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference/#create-stream-marker
|
||||
Requires `user:edit:broadcast` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp createStreamMarker`
|
||||
Used in:
|
||||
- `controllers/commands/CommandController.cpp` in /marker command
|
||||
Used in:
|
||||
|
||||
- `controllers/commands/CommandController.cpp` in /marker command
|
||||
|
||||
### Get User Block List
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-user-block-list
|
||||
Requires `user:read:blocked_users` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp loadBlocks`
|
||||
Used in:
|
||||
- `providers/twitch/TwitchAccount.cpp loadBlocks` to load list of blocked (blocked) users by current user
|
||||
Used in:
|
||||
|
||||
- `providers/twitch/TwitchAccount.cpp loadBlocks` to load list of blocked (blocked) users by current user
|
||||
|
||||
### Block User
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#block-user
|
||||
Requires `user:manage:blocked_users` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp blockUser`
|
||||
Used in:
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to block a user via checkbox in the usercard
|
||||
- `controllers/commands/CommandController.cpp` to block a user via "/block" command
|
||||
Used in:
|
||||
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to block a user via checkbox in the usercard
|
||||
- `controllers/commands/CommandController.cpp` to block a user via "/block" command
|
||||
|
||||
### Unblock User
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#unblock-user
|
||||
Requires `user:manage:blocked_users` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp unblockUser`
|
||||
Used in:
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to unblock a user via checkbox in the usercard
|
||||
- `controllers/commands/CommandController.cpp` to unblock a user via "/unblock" command
|
||||
Used in:
|
||||
|
||||
- `widgets/dialogs/UserInfoPopup.cpp` to unblock a user via checkbox in the usercard
|
||||
- `controllers/commands/CommandController.cpp` to unblock a user via "/unblock" command
|
||||
|
||||
### Search Categories
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#search-categories
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp searchGames`
|
||||
Used in:
|
||||
- `controllers/commands/CommandController.cpp` in `/setgame` command to fuzzy search for game titles
|
||||
Used in:
|
||||
|
||||
- `controllers/commands/CommandController.cpp` in `/setgame` command to fuzzy search for game titles
|
||||
|
||||
### Manage Held AutoMod Messages
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages
|
||||
Requires `moderator:manage:automod` scope
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp manageAutoModMessages`
|
||||
Used in:
|
||||
- `providers/twitch/TwitchAccount.cpp` to approve/deny held AutoMod messages
|
||||
Used in:
|
||||
|
||||
- `providers/twitch/TwitchAccount.cpp` to approve/deny held AutoMod messages
|
||||
|
||||
### Get Cheermotes
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference/#get-cheermotes
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp getCheermotes`
|
||||
Used in:
|
||||
- `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
||||
Used in:
|
||||
|
||||
- `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
||||
|
||||
### Get Emote Sets
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp getEmoteSetData`
|
||||
Used in:
|
||||
- `providers/twitch/TwitchAccount.cpp` to set emoteset owner data upon loading subscriber emotes from Kraken
|
||||
Not used anywhere at the moment. Could be useful in the future for loading emotes from Helix.
|
||||
|
||||
### Get Channel Emotes
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-channel-emotes
|
||||
|
||||
- We implement this in `providers/twitch/api/Helix.cpp getChannelEmotes`
|
||||
Not used anywhere at the moment.
|
||||
Not used anywhere at the moment.
|
||||
|
||||
## TMI
|
||||
|
||||
|
|
|
@ -240,8 +240,8 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
|
|||
postToThread([=] {
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
app->twitch.server->watchingChannel.reset(
|
||||
app->twitch.server->getOrAddChannel(name));
|
||||
app->twitch->watchingChannel.reset(
|
||||
app->twitch->getOrAddChannel(name));
|
||||
}
|
||||
|
||||
if (attach || attachFullscreen)
|
||||
|
@ -252,8 +252,7 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
|
|||
AttachedWindow::get(::GetForegroundWindow(), args);
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
window->setChannel(
|
||||
app->twitch.server->getOrAddChannel(name));
|
||||
window->setChannel(app->twitch->getOrAddChannel(name));
|
||||
}
|
||||
// }
|
||||
// window->show();
|
||||
|
|
|
@ -98,8 +98,8 @@ public:
|
|||
"/appearance/messages/usernameDisplayMode",
|
||||
UsernameDisplayMode::UsernameAndLocalizedName};
|
||||
|
||||
IntSetting tabDirection = {"/appearance/tabDirection",
|
||||
NotebookTabDirection::Horizontal};
|
||||
EnumSetting<NotebookTabDirection> tabDirection = {
|
||||
"/appearance/tabDirection", NotebookTabDirection::Horizontal};
|
||||
|
||||
// BoolSetting collapseLongMessages =
|
||||
// {"/appearance/messages/collapseLongMessages", false};
|
||||
|
|
|
@ -62,8 +62,11 @@ void Theme::actuallyUpdate(double hue, double multiplier)
|
|||
this->splits.header.background = getColor(0, sat, flat ? 1 : 0.9);
|
||||
this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85);
|
||||
this->splits.header.text = this->messages.textColors.regular;
|
||||
this->splits.header.focusedText =
|
||||
isLight ? QColor("#198CFF") : QColor("#84C1FF");
|
||||
this->splits.header.focusedBackground =
|
||||
getColor(0, sat, isLight ? 0.95 : 0.79);
|
||||
this->splits.header.focusedBorder = getColor(0, sat, isLight ? 0.90 : 0.78);
|
||||
this->splits.header.focusedText = QColor::fromHsvF(
|
||||
0.58388, isLight ? 1.0 : 0.482, isLight ? 0.6375 : 1.0);
|
||||
|
||||
this->splits.input.background = getColor(0, sat, flat ? 0.95 : 0.95);
|
||||
this->splits.input.border = getColor(0, sat, flat ? 1 : 1);
|
||||
|
|
|
@ -31,7 +31,9 @@ public:
|
|||
|
||||
struct {
|
||||
QColor border;
|
||||
QColor focusedBorder;
|
||||
QColor background;
|
||||
QColor focusedBackground;
|
||||
QColor text;
|
||||
QColor focusedText;
|
||||
// int margin;
|
||||
|
|
|
@ -605,23 +605,23 @@ IndirectChannel WindowManager::decodeChannel(const SplitDescriptor &descriptor)
|
|||
|
||||
if (descriptor.type_ == "twitch")
|
||||
{
|
||||
return app->twitch.server->getOrAddChannel(descriptor.channelName_);
|
||||
return app->twitch->getOrAddChannel(descriptor.channelName_);
|
||||
}
|
||||
else if (descriptor.type_ == "mentions")
|
||||
{
|
||||
return app->twitch.server->mentionsChannel;
|
||||
return app->twitch->mentionsChannel;
|
||||
}
|
||||
else if (descriptor.type_ == "watching")
|
||||
{
|
||||
return app->twitch.server->watchingChannel;
|
||||
return app->twitch->watchingChannel;
|
||||
}
|
||||
else if (descriptor.type_ == "whispers")
|
||||
{
|
||||
return app->twitch.server->whispersChannel;
|
||||
return app->twitch->whispersChannel;
|
||||
}
|
||||
else if (descriptor.type_ == "live")
|
||||
{
|
||||
return app->twitch.server->liveChannel;
|
||||
return app->twitch->liveChannel;
|
||||
}
|
||||
else if (descriptor.type_ == "irc")
|
||||
{
|
||||
|
|
|
@ -12,10 +12,11 @@ namespace {
|
|||
{
|
||||
// list of command line switches to turn on private browsing in browsers
|
||||
static auto switches = std::vector<std::pair<QString, QString>>{
|
||||
{"firefox", "-private-window"}, {"chrome", "-incognito"},
|
||||
{"vivaldi", "-incognito"}, {"opera", "-newprivatetab"},
|
||||
{"opera\\\\launcher", "--private"}, {"iexplore", "-private"},
|
||||
{"msedge", "-inprivate"},
|
||||
{"firefox", "-private-window"}, {"librewolf", "-private-window"},
|
||||
{"waterfox", "-private-window"}, {"icecat", "-private-window"},
|
||||
{"chrome", "-incognito"}, {"vivaldi", "-incognito"},
|
||||
{"opera", "-newprivatetab"}, {"opera\\\\launcher", "--private"},
|
||||
{"iexplore", "-private"}, {"msedge", "-inprivate"},
|
||||
};
|
||||
|
||||
// transform into regex and replacement string
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
#include "util/StreamLink.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "providers/irc/IrcMessageBuilder.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/SplitCommand.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
#include "widgets/dialogs/QualityPopup.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
|
||||
#include <QErrorMessage>
|
||||
#include <QFileInfo>
|
||||
|
@ -205,6 +209,20 @@ void openStreamlink(const QString &channelURL, const QString &quality,
|
|||
|
||||
void openStreamlinkForChannel(const QString &channel)
|
||||
{
|
||||
static const QString INFO_TEMPLATE("Opening %1 in Streamlink ...");
|
||||
|
||||
auto *currentPage = dynamic_cast<SplitContainer *>(
|
||||
getApp()->windows->getMainWindow().getNotebook().getSelectedPage());
|
||||
if (currentPage != nullptr)
|
||||
{
|
||||
if (auto currentSplit = currentPage->getSelectedSplit();
|
||||
currentSplit != nullptr)
|
||||
{
|
||||
currentSplit->getChannel()->addMessage(
|
||||
makeSystemMessage(INFO_TEMPLATE.arg(channel)));
|
||||
}
|
||||
}
|
||||
|
||||
QString channelURL = "twitch.tv/" + channel;
|
||||
|
||||
QString preferredQuality = getSettings()->preferredQuality.getValue();
|
||||
|
|
|
@ -76,7 +76,7 @@ bool isInStreamerMode()
|
|||
{
|
||||
shouldShowWarning = false;
|
||||
|
||||
getApp()->twitch2->addGlobalSystemMessage(
|
||||
getApp()->twitch->addGlobalSystemMessage(
|
||||
"Streamer Mode is set to Automatic, but pgrep is missing. "
|
||||
"Install it to fix the issue or set Streamer Mode to "
|
||||
"Enabled or Disabled in the Settings.");
|
||||
|
|
|
@ -10,4 +10,29 @@ void openTwitchUsercard(QString channel, QString username)
|
|||
QDesktopServices::openUrl("https://www.twitch.tv/popout/" + channel +
|
||||
"/viewercard/" + username);
|
||||
}
|
||||
|
||||
void stripUserName(QString &userName)
|
||||
{
|
||||
if (userName.startsWith('@'))
|
||||
{
|
||||
userName.remove(0, 1);
|
||||
}
|
||||
if (userName.endsWith(','))
|
||||
{
|
||||
userName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
void stripChannelName(QString &channelName)
|
||||
{
|
||||
if (channelName.startsWith('@') || channelName.startsWith('#'))
|
||||
{
|
||||
channelName.remove(0, 1);
|
||||
}
|
||||
if (channelName.endsWith(','))
|
||||
{
|
||||
channelName.chop(1);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -6,4 +6,10 @@ namespace chatterino {
|
|||
|
||||
void openTwitchUsercard(const QString channel, const QString username);
|
||||
|
||||
// stripUserName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripUserName(QString &userName);
|
||||
|
||||
// stripChannelName removes any @ prefix or , suffix to make it more suitable for command use
|
||||
void stripChannelName(QString &channelName);
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -130,8 +130,6 @@ float BaseWindow::qtFontScale() const
|
|||
|
||||
void BaseWindow::init()
|
||||
{
|
||||
this->setWindowIcon(QIcon(":/images/icon.png"));
|
||||
|
||||
#ifdef USEWINSDK
|
||||
if (this->hasCustomWindowFrame())
|
||||
{
|
||||
|
|
|
@ -49,7 +49,7 @@ bool FramelessEmbedWindow::nativeEvent(const QByteArray &eventType,
|
|||
auto channelName = root.value("channel-name").toString();
|
||||
|
||||
this->split_->setChannel(
|
||||
getApp()->twitch2->getOrAddChannel(channelName));
|
||||
getApp()->twitch->getOrAddChannel(channelName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -254,7 +254,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
static int index = 0;
|
||||
auto app = getApp();
|
||||
const auto &msg = messages[index++ % messages.size()];
|
||||
app->twitch.server->addFakeMessage(msg);
|
||||
app->twitch->addFakeMessage(msg);
|
||||
return "";
|
||||
});
|
||||
|
||||
|
@ -262,7 +262,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
const auto &messages = cheerMessages;
|
||||
static int index = 0;
|
||||
const auto &msg = messages[index++ % messages.size()];
|
||||
getApp()->twitch.server->addFakeMessage(msg);
|
||||
getApp()->twitch->addFakeMessage(msg);
|
||||
return "";
|
||||
});
|
||||
|
||||
|
@ -271,7 +271,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
static int index = 0;
|
||||
auto app = getApp();
|
||||
const auto &msg = messages[index++ % messages.size()];
|
||||
app->twitch.server->addFakeMessage(msg);
|
||||
app->twitch->addFakeMessage(msg);
|
||||
return "";
|
||||
});
|
||||
|
||||
|
@ -282,15 +282,15 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
if (alt)
|
||||
{
|
||||
doc.Parse(channelRewardMessage);
|
||||
app->twitch.server->addFakeMessage(channelRewardIRCMessage);
|
||||
app->twitch.pubsub->signals_.pointReward.redeemed.invoke(
|
||||
app->twitch->addFakeMessage(channelRewardIRCMessage);
|
||||
app->twitch->pubsub->signals_.pointReward.redeemed.invoke(
|
||||
doc["data"]["message"]["data"]["redemption"]);
|
||||
alt = !alt;
|
||||
}
|
||||
else
|
||||
{
|
||||
doc.Parse(channelRewardMessage2);
|
||||
app->twitch.pubsub->signals_.pointReward.redeemed.invoke(
|
||||
app->twitch->pubsub->signals_.pointReward.redeemed.invoke(
|
||||
doc["data"]["message"]["data"]["redemption"]);
|
||||
alt = !alt;
|
||||
}
|
||||
|
@ -301,7 +301,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
const auto &messages = emoteTestMessages;
|
||||
static int index = 0;
|
||||
const auto &msg = messages[index++ % messages.size()];
|
||||
getApp()->twitch.server->addFakeMessage(msg);
|
||||
getApp()->twitch->addFakeMessage(msg);
|
||||
return "";
|
||||
});
|
||||
#endif
|
||||
|
@ -466,7 +466,7 @@ void Window::addShortcuts()
|
|||
this->notebook_->select(splitContainer);
|
||||
Split *split = new Split(splitContainer);
|
||||
split->setChannel(
|
||||
getApp()->twitch.server->getOrAddChannel(si.channelName));
|
||||
getApp()->twitch->getOrAddChannel(si.channelName));
|
||||
split->setFilters(si.filters);
|
||||
splitContainer->appendSplit(split);
|
||||
return "";
|
||||
|
|
|
@ -341,9 +341,9 @@ void EmotePopup::loadChannel(ChannelPtr channel)
|
|||
*globalChannel, *subChannel, this->channel_->getName());
|
||||
|
||||
// global
|
||||
addEmotes(*globalChannel, *getApp()->twitch2->getBttvEmotes().emotes(),
|
||||
addEmotes(*globalChannel, *getApp()->twitch->getBttvEmotes().emotes(),
|
||||
"BetterTTV", MessageElementFlag::BttvEmote);
|
||||
addEmotes(*globalChannel, *getApp()->twitch2->getFfzEmotes().emotes(),
|
||||
addEmotes(*globalChannel, *getApp()->twitch->getFfzEmotes().emotes(),
|
||||
"FrankerFaceZ", MessageElementFlag::FfzEmote);
|
||||
|
||||
// channel
|
||||
|
@ -383,18 +383,9 @@ void EmotePopup::loadEmojis(Channel &channel, EmojiMap &emojiMap,
|
|||
channel.addMessage(makeEmojiMessage(emojiMap));
|
||||
}
|
||||
|
||||
void EmotePopup::filterEmotes(const QString &searchText)
|
||||
void EmotePopup::filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
||||
const QString &searchText)
|
||||
{
|
||||
if (searchText.length() == 0)
|
||||
{
|
||||
this->notebook_->show();
|
||||
this->searchView_->hide();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto searchChannel = std::make_shared<Channel>("", Channel::Type::None);
|
||||
|
||||
auto twitchEmoteSets =
|
||||
getApp()->accounts->twitch.getCurrent()->accessEmotes()->emoteSets;
|
||||
std::vector<std::shared_ptr<TwitchAccount::EmoteSet>> twitchGlobalEmotes{};
|
||||
|
@ -415,25 +406,9 @@ void EmotePopup::filterEmotes(const QString &searchText)
|
|||
}
|
||||
|
||||
auto bttvGlobalEmotes = this->filterEmoteMap(
|
||||
searchText, getApp()->twitch2->getBttvEmotes().emotes());
|
||||
searchText, getApp()->twitch->getBttvEmotes().emotes());
|
||||
auto ffzGlobalEmotes = this->filterEmoteMap(
|
||||
searchText, getApp()->twitch2->getFfzEmotes().emotes());
|
||||
auto bttvChannelEmotes =
|
||||
this->filterEmoteMap(searchText, this->twitchChannel_->bttvEmotes());
|
||||
auto ffzChannelEmotes =
|
||||
this->filterEmoteMap(searchText, this->twitchChannel_->ffzEmotes());
|
||||
|
||||
EmojiMap filteredEmojis{};
|
||||
int emojiCount = 0;
|
||||
|
||||
getApp()->emotes->emojis.emojis.each(
|
||||
[&, searchText](const auto &name, std::shared_ptr<EmojiData> &emoji) {
|
||||
if (emoji->shortCodes[0].contains(searchText, Qt::CaseInsensitive))
|
||||
{
|
||||
filteredEmojis.insert(name, emoji);
|
||||
emojiCount++;
|
||||
}
|
||||
});
|
||||
searchText, getApp()->twitch->getFfzEmotes().emotes());
|
||||
|
||||
// twitch
|
||||
addEmoteSets(twitchGlobalEmotes, *searchChannel, *searchChannel,
|
||||
|
@ -447,6 +422,15 @@ void EmotePopup::filterEmotes(const QString &searchText)
|
|||
addEmotes(*searchChannel, *ffzGlobalEmotes, "FrankerFaceZ (Global)",
|
||||
MessageElementFlag::FfzEmote);
|
||||
|
||||
if (!this->twitchChannel_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto bttvChannelEmotes =
|
||||
this->filterEmoteMap(searchText, this->twitchChannel_->bttvEmotes());
|
||||
auto ffzChannelEmotes =
|
||||
this->filterEmoteMap(searchText, this->twitchChannel_->ffzEmotes());
|
||||
// channel
|
||||
if (bttvChannelEmotes->size() > 0)
|
||||
addEmotes(*searchChannel, *bttvChannelEmotes, "BetterTTV (Channel)",
|
||||
|
@ -454,7 +438,36 @@ void EmotePopup::filterEmotes(const QString &searchText)
|
|||
if (ffzChannelEmotes->size() > 0)
|
||||
addEmotes(*searchChannel, *ffzChannelEmotes, "FrankerFaceZ (Channel)",
|
||||
MessageElementFlag::FfzEmote);
|
||||
}
|
||||
|
||||
void EmotePopup::filterEmotes(const QString &searchText)
|
||||
{
|
||||
if (searchText.length() == 0)
|
||||
{
|
||||
this->notebook_->show();
|
||||
this->searchView_->hide();
|
||||
|
||||
return;
|
||||
}
|
||||
auto searchChannel = std::make_shared<Channel>("", Channel::Type::None);
|
||||
|
||||
// true in special channels like /mentions
|
||||
if (this->channel_->isTwitchChannel())
|
||||
{
|
||||
this->filterTwitchEmotes(searchChannel, searchText);
|
||||
}
|
||||
|
||||
EmojiMap filteredEmojis{};
|
||||
int emojiCount = 0;
|
||||
|
||||
getApp()->emotes->emojis.emojis.each(
|
||||
[&, searchText](const auto &name, std::shared_ptr<EmojiData> &emoji) {
|
||||
if (emoji->shortCodes[0].contains(searchText, Qt::CaseInsensitive))
|
||||
{
|
||||
filteredEmojis.insert(name, emoji);
|
||||
emojiCount++;
|
||||
}
|
||||
});
|
||||
// emojis
|
||||
if (emojiCount > 0)
|
||||
this->loadEmojis(*searchChannel, filteredEmojis, "Emojis");
|
||||
|
|
|
@ -46,6 +46,8 @@ private:
|
|||
|
||||
void loadEmojis(ChannelView &view, EmojiMap &emojiMap);
|
||||
void loadEmojis(Channel &channel, EmojiMap &emojiMap, const QString &title);
|
||||
void filterTwitchEmotes(std::shared_ptr<Channel> searchChannel,
|
||||
const QString &searchText);
|
||||
void filterEmotes(const QString &text);
|
||||
EmoteMap *filterEmoteMap(const QString &text,
|
||||
std::shared_ptr<const EmoteMap> emotes);
|
||||
|
|
|
@ -350,24 +350,24 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const
|
|||
case TAB_TWITCH: {
|
||||
if (this->ui_.twitch.channel->isChecked())
|
||||
{
|
||||
return app->twitch.server->getOrAddChannel(
|
||||
return app->twitch->getOrAddChannel(
|
||||
this->ui_.twitch.channelName->text().trimmed());
|
||||
}
|
||||
else if (this->ui_.twitch.watching->isChecked())
|
||||
{
|
||||
return app->twitch.server->watchingChannel;
|
||||
return app->twitch->watchingChannel;
|
||||
}
|
||||
else if (this->ui_.twitch.mentions->isChecked())
|
||||
{
|
||||
return app->twitch.server->mentionsChannel;
|
||||
return app->twitch->mentionsChannel;
|
||||
}
|
||||
else if (this->ui_.twitch.whispers->isChecked())
|
||||
{
|
||||
return app->twitch.server->whispersChannel;
|
||||
return app->twitch->whispersChannel;
|
||||
}
|
||||
else if (this->ui_.twitch.live->isChecked())
|
||||
{
|
||||
return app->twitch.server->liveChannel;
|
||||
return app->twitch->liveChannel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -407,8 +407,6 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
|
|||
|
||||
if (event->type() == QEvent::FocusIn)
|
||||
{
|
||||
widget->grabKeyboard();
|
||||
|
||||
auto *radio = dynamic_cast<QRadioButton *>(watched);
|
||||
if (radio)
|
||||
{
|
||||
|
@ -417,11 +415,6 @@ bool SelectChannelDialog::EventFilter::eventFilter(QObject *watched,
|
|||
|
||||
return true;
|
||||
}
|
||||
else if (event->type() == QEvent::FocusOut)
|
||||
{
|
||||
widget->releaseKeyboard();
|
||||
return false;
|
||||
}
|
||||
else if (event->type() == QEvent::KeyPress)
|
||||
{
|
||||
QKeyEvent *event_key = static_cast<QKeyEvent *>(event);
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/IvrApi.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/api/Kraken.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
#include "util/Clipboard.hpp"
|
||||
#include "util/Helpers.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
|
@ -23,9 +24,11 @@
|
|||
#include "util/StreamerMode.hpp"
|
||||
#include "widgets/Label.hpp"
|
||||
#include "widgets/Scrollbar.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
#include "widgets/helper/ChannelView.hpp"
|
||||
#include "widgets/helper/EffectLabel.hpp"
|
||||
#include "widgets/helper/Line.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDesktopServices>
|
||||
|
@ -133,6 +136,7 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
|||
: userInfoPopupFlags,
|
||||
parent)
|
||||
, hack_(new bool)
|
||||
, dragTimer_(this)
|
||||
{
|
||||
this->setWindowTitle("Usercard");
|
||||
this->setStayInScreenRect(true);
|
||||
|
@ -173,6 +177,58 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
|||
}
|
||||
return "";
|
||||
}},
|
||||
{"execModeratorAction",
|
||||
[this](std::vector<QString> arguments) -> QString {
|
||||
if (arguments.empty())
|
||||
{
|
||||
return "execModeratorAction action needs an argument, which "
|
||||
"moderation action to execute, see description in the "
|
||||
"editor";
|
||||
}
|
||||
auto target = arguments.at(0);
|
||||
QString msg;
|
||||
|
||||
// these can't have /timeout/ buttons because they are not timeouts
|
||||
if (target == "ban")
|
||||
{
|
||||
msg = QString("/ban %1").arg(this->userName_);
|
||||
}
|
||||
else if (target == "unban")
|
||||
{
|
||||
msg = QString("/unban %1").arg(this->userName_);
|
||||
}
|
||||
else
|
||||
{
|
||||
// find and execute timeout button #TARGET
|
||||
|
||||
bool ok;
|
||||
int buttonNum = target.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
return QString("Invalid argument for execModeratorAction: "
|
||||
"%1. Use "
|
||||
"\"ban\", \"unban\" or the number of the "
|
||||
"timeout "
|
||||
"button to execute")
|
||||
.arg(target);
|
||||
}
|
||||
|
||||
const auto &timeoutButtons =
|
||||
getSettings()->timeoutButtons.getValue();
|
||||
if (timeoutButtons.size() < buttonNum || 0 >= buttonNum)
|
||||
{
|
||||
return QString("Invalid argument for execModeratorAction: "
|
||||
"%1. Integer out of usable range: [1, %2]")
|
||||
.arg(buttonNum, timeoutButtons.size() - 1);
|
||||
}
|
||||
const auto &button = timeoutButtons.at(buttonNum - 1);
|
||||
msg = QString("/timeout %1 %2")
|
||||
.arg(this->userName_)
|
||||
.arg(calculateTimeoutDuration(button));
|
||||
}
|
||||
this->channel_->sendMessage(msg);
|
||||
return "";
|
||||
}},
|
||||
|
||||
// these actions make no sense in the context of a usercard, so they aren't implemented
|
||||
{"reject", nullptr},
|
||||
|
@ -234,6 +290,21 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
|||
crossPlatformCopy(avatarUrl);
|
||||
});
|
||||
|
||||
// we need to assign login name for msvc compilation
|
||||
auto loginName = this->userName_.toLower();
|
||||
menu->addAction(
|
||||
"Open channel in a new popup window", this,
|
||||
[loginName] {
|
||||
auto app = getApp();
|
||||
auto &window = app->windows->createWindow(
|
||||
WindowType::Popup, true);
|
||||
auto split = window.getNotebook()
|
||||
.getOrAddSelectedPage()
|
||||
->appendNewSplit(false);
|
||||
split->setChannel(app->twitch->getOrAddChannel(
|
||||
loginName.toLower()));
|
||||
});
|
||||
|
||||
menu->popup(QCursor::pos());
|
||||
menu->raise();
|
||||
}
|
||||
|
@ -428,6 +499,21 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, QWidget *parent)
|
|||
|
||||
this->installEvents();
|
||||
this->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Policy::Ignored);
|
||||
|
||||
this->dragTimer_.callOnTimeout(
|
||||
[this, hack = std::weak_ptr<bool>(this->hack_)] {
|
||||
if (!hack.lock())
|
||||
{
|
||||
// Ensure this timer is never called after the object has been destroyed
|
||||
return;
|
||||
}
|
||||
if (!this->isMoving_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->move(this->requestedDragPos_);
|
||||
});
|
||||
}
|
||||
|
||||
void UserInfoPopup::themeChangedEvent()
|
||||
|
@ -453,6 +539,36 @@ void UserInfoPopup::scaleChangedEvent(float /*scale*/)
|
|||
});
|
||||
}
|
||||
|
||||
void UserInfoPopup::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::MouseButton::LeftButton)
|
||||
{
|
||||
this->dragTimer_.start(std::chrono::milliseconds(17));
|
||||
this->startPosDrag_ = event->pos();
|
||||
this->movingRelativePos = event->localPos();
|
||||
}
|
||||
}
|
||||
|
||||
void UserInfoPopup::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
this->dragTimer_.stop();
|
||||
this->isMoving_ = false;
|
||||
}
|
||||
|
||||
void UserInfoPopup::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
// Drag the window by the amount changed from inital position
|
||||
// Note that we provide a few *units* of deadzone so people don't
|
||||
// start dragging the window if they are slow at clicking.
|
||||
auto movePos = event->pos() - this->startPosDrag_;
|
||||
if (this->isMoving_ || movePos.manhattanLength() > 10.0)
|
||||
{
|
||||
this->requestedDragPos_ =
|
||||
(event->screenPos() - this->movingRelativePos).toPoint();
|
||||
this->isMoving_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UserInfoPopup::installEvents()
|
||||
{
|
||||
std::shared_ptr<bool> ignoreNext = std::make_shared<bool>(false);
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include <pajlada/signals/scoped-connection.hpp>
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
class QCheckBox;
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -26,6 +28,9 @@ public:
|
|||
protected:
|
||||
virtual void themeChangedEvent() override;
|
||||
virtual void scaleChangedEvent(float scale) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void installEvents();
|
||||
|
@ -41,6 +46,19 @@ private:
|
|||
QString avatarUrl_;
|
||||
ChannelPtr channel_;
|
||||
|
||||
// isMoving_ is set to true if the user is holding the left mouse button down and has moved the mouse a small amount away from the original click point (startPosDrag_)
|
||||
bool isMoving_ = false;
|
||||
|
||||
// startPosDrag_ is the coordinates where the user originally pressed the mouse button down to start dragging
|
||||
QPoint startPosDrag_;
|
||||
|
||||
// requestDragPos_ is the final screen coordinates where the widget should be moved to.
|
||||
// Takes the relative position of where the user originally clicked the widget into account
|
||||
QPoint requestedDragPos_;
|
||||
|
||||
// dragTimer_ is called ~60 times per second once the user has initiated dragging
|
||||
QTimer dragTimer_;
|
||||
|
||||
pajlada::Signals::NoArgSignal userStateChanged_;
|
||||
|
||||
std::unique_ptr<pajlada::Signals::ScopedConnection> refreshConnection_;
|
||||
|
|
|
@ -25,8 +25,7 @@ void NewTabItem::action()
|
|||
SplitContainer *container = nb.addPage(true);
|
||||
|
||||
Split *split = new Split(container);
|
||||
split->setChannel(
|
||||
getApp()->twitch.server->getOrAddChannel(this->channelName_));
|
||||
split->setChannel(getApp()->twitch->getOrAddChannel(this->channelName_));
|
||||
container->appendSplit(split);
|
||||
}
|
||||
|
||||
|
|
|
@ -1016,15 +1016,15 @@ MessageElementFlags ChannelView::getFlags() const
|
|||
{
|
||||
flags.set(MessageElementFlag::ModeratorTools);
|
||||
}
|
||||
if (this->underlyingChannel_ == app->twitch.server->mentionsChannel ||
|
||||
this->underlyingChannel_ == app->twitch.server->liveChannel)
|
||||
if (this->underlyingChannel_ == app->twitch->mentionsChannel ||
|
||||
this->underlyingChannel_ == app->twitch->liveChannel)
|
||||
{
|
||||
flags.set(MessageElementFlag::ChannelName);
|
||||
flags.unset(MessageElementFlag::ChannelPointReward);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->sourceChannel_ == app->twitch.server->mentionsChannel)
|
||||
if (this->sourceChannel_ == app->twitch->mentionsChannel)
|
||||
flags.set(MessageElementFlag::ChannelName);
|
||||
|
||||
return flags;
|
||||
|
@ -1071,8 +1071,7 @@ void ChannelView::drawMessages(QPainter &painter)
|
|||
bool windowFocused = this->window() == QApplication::activeWindow();
|
||||
|
||||
auto app = getApp();
|
||||
bool isMentions =
|
||||
this->underlyingChannel_ == app->twitch.server->mentionsChannel;
|
||||
bool isMentions = this->underlyingChannel_ == app->twitch->mentionsChannel;
|
||||
|
||||
for (size_t i = start; i < messagesSnapshot.size(); ++i)
|
||||
{
|
||||
|
@ -1762,11 +1761,6 @@ void ChannelView::mouseReleaseEvent(QMouseEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
if (hoverLayoutElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// handle the click
|
||||
this->handleMouseClick(event, hoverLayoutElement, layout);
|
||||
|
||||
|
@ -1790,7 +1784,12 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
|
|||
this->queueLayout();
|
||||
}
|
||||
|
||||
auto &link = hoveredElement->getLink();
|
||||
if (hoveredElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &link = hoveredElement->getLink();
|
||||
if (!getSettings()->linksDoubleClickOnly)
|
||||
{
|
||||
this->handleLinkClick(event, link, layout.get());
|
||||
|
@ -1812,28 +1811,39 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
|
|||
}
|
||||
};
|
||||
|
||||
auto &link = hoveredElement->getLink();
|
||||
if (link.type == Link::UserInfo)
|
||||
if (hoveredElement != nullptr)
|
||||
{
|
||||
const bool commaMention = getSettings()->mentionUsersWithComma;
|
||||
const bool isFirstWord =
|
||||
split && split->getInput().isEditFirstWord();
|
||||
auto userMention =
|
||||
formatUserMention(link.value, isFirstWord, commaMention);
|
||||
insertText("@" + userMention + " ");
|
||||
}
|
||||
else if (link.type == Link::UserWhisper)
|
||||
{
|
||||
insertText("/w " + link.value + " ");
|
||||
}
|
||||
else
|
||||
{
|
||||
this->addContextMenuItems(hoveredElement, layout);
|
||||
const auto &link = hoveredElement->getLink();
|
||||
|
||||
if (link.type == Link::UserInfo)
|
||||
{
|
||||
const bool commaMention =
|
||||
getSettings()->mentionUsersWithComma;
|
||||
const bool isFirstWord =
|
||||
split && split->getInput().isEditFirstWord();
|
||||
auto userMention = formatUserMention(
|
||||
link.value, isFirstWord, commaMention);
|
||||
insertText("@" + userMention + " ");
|
||||
return;
|
||||
}
|
||||
|
||||
if (link.type == Link::UserWhisper)
|
||||
{
|
||||
insertText("/w " + link.value + " ");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->addContextMenuItems(hoveredElement, layout, event);
|
||||
}
|
||||
break;
|
||||
case Qt::MiddleButton: {
|
||||
auto &link = hoveredElement->getLink();
|
||||
if (hoveredElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &link = hoveredElement->getLink();
|
||||
if (!getSettings()->linksDoubleClickOnly)
|
||||
{
|
||||
this->handleLinkClick(event, link, layout.get());
|
||||
|
@ -1845,11 +1855,9 @@ void ChannelView::handleMouseClick(QMouseEvent *event,
|
|||
}
|
||||
|
||||
void ChannelView::addContextMenuItems(
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout)
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout,
|
||||
QMouseEvent *event)
|
||||
{
|
||||
const auto &creator = hoveredElement->getCreator();
|
||||
auto creatorFlags = creator.getFlags();
|
||||
|
||||
static QMenu *previousMenu = nullptr;
|
||||
if (previousMenu != nullptr)
|
||||
{
|
||||
|
@ -1860,12 +1868,45 @@ void ChannelView::addContextMenuItems(
|
|||
auto menu = new QMenu;
|
||||
previousMenu = menu;
|
||||
|
||||
// Add image options if the element clicked contains an image (e.g. a badge or an emote)
|
||||
this->addImageContextMenuItems(hoveredElement, layout, event, *menu);
|
||||
|
||||
// Add link options if the element clicked contains a link
|
||||
this->addLinkContextMenuItems(hoveredElement, layout, event, *menu);
|
||||
|
||||
// Add message options
|
||||
this->addMessageContextMenuItems(hoveredElement, layout, event, *menu);
|
||||
|
||||
// Add Twitch-specific link options if the element clicked contains a link detected as a Twitch username
|
||||
this->addTwitchLinkContextMenuItems(hoveredElement, layout, event, *menu);
|
||||
|
||||
// Add hidden options (e.g. copy message ID) if the user held down Shift
|
||||
this->addHiddenContextMenuItems(hoveredElement, layout, event, *menu);
|
||||
|
||||
menu->popup(QCursor::pos());
|
||||
menu->raise();
|
||||
}
|
||||
|
||||
void ChannelView::addImageContextMenuItems(
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/,
|
||||
QMouseEvent * /*event*/, QMenu &menu)
|
||||
{
|
||||
if (hoveredElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &creator = hoveredElement->getCreator();
|
||||
auto creatorFlags = creator.getFlags();
|
||||
|
||||
// Badge actions
|
||||
if (creatorFlags.hasAny({MessageElementFlag::Badges}))
|
||||
{
|
||||
if (auto badgeElement = dynamic_cast<const BadgeElement *>(&creator))
|
||||
{
|
||||
addEmoteContextMenuItems(*badgeElement->getEmote(), creatorFlags,
|
||||
*menu);
|
||||
menu);
|
||||
}
|
||||
}
|
||||
|
||||
// Emote actions
|
||||
|
@ -1873,48 +1914,68 @@ void ChannelView::addContextMenuItems(
|
|||
{MessageElementFlag::EmoteImages, MessageElementFlag::EmojiImage}))
|
||||
{
|
||||
if (auto emoteElement = dynamic_cast<const EmoteElement *>(&creator))
|
||||
{
|
||||
addEmoteContextMenuItems(*emoteElement->getEmote(), creatorFlags,
|
||||
*menu);
|
||||
menu);
|
||||
}
|
||||
}
|
||||
|
||||
// add seperator
|
||||
if (!menu->actions().empty())
|
||||
if (!menu.actions().empty())
|
||||
{
|
||||
menu->addSeparator();
|
||||
menu.addSeparator();
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelView::addLinkContextMenuItems(
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/,
|
||||
QMouseEvent * /*event*/, QMenu &menu)
|
||||
{
|
||||
if (hoveredElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &link = hoveredElement->getLink();
|
||||
|
||||
if (link.type != Link::Url)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Link copy
|
||||
if (hoveredElement->getLink().type == Link::Url)
|
||||
QString url = link.value;
|
||||
|
||||
// open link
|
||||
menu.addAction("Open link", [url] {
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
});
|
||||
// open link default
|
||||
if (supportsIncognitoLinks())
|
||||
{
|
||||
QString url = hoveredElement->getLink().value;
|
||||
|
||||
// open link
|
||||
menu->addAction("Open link", [url] {
|
||||
QDesktopServices::openUrl(QUrl(url));
|
||||
menu.addAction("Open link incognito", [url] {
|
||||
openLinkIncognito(url);
|
||||
});
|
||||
// open link default
|
||||
if (supportsIncognitoLinks())
|
||||
{
|
||||
menu->addAction("Open link incognito", [url] {
|
||||
openLinkIncognito(url);
|
||||
});
|
||||
}
|
||||
menu->addAction("Copy link", [url] {
|
||||
crossPlatformCopy(url);
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
}
|
||||
menu.addAction("Copy link", [url] {
|
||||
crossPlatformCopy(url);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
}
|
||||
void ChannelView::addMessageContextMenuItems(
|
||||
const MessageLayoutElement * /*hoveredElement*/, MessageLayoutPtr layout,
|
||||
QMouseEvent * /*event*/, QMenu &menu)
|
||||
{
|
||||
// Copy actions
|
||||
if (!this->selection_.isEmpty())
|
||||
{
|
||||
menu->addAction("Copy selection", [this] {
|
||||
menu.addAction("Copy selection", [this] {
|
||||
crossPlatformCopy(this->getSelectedText());
|
||||
});
|
||||
}
|
||||
|
||||
menu->addAction("Copy message", [layout] {
|
||||
menu.addAction("Copy message", [layout] {
|
||||
QString copyString;
|
||||
layout->addSelectionText(copyString, 0, INT_MAX,
|
||||
CopyMode::OnlyTextAndEmotes);
|
||||
|
@ -1922,75 +1983,109 @@ void ChannelView::addContextMenuItems(
|
|||
crossPlatformCopy(copyString);
|
||||
});
|
||||
|
||||
menu->addAction("Copy full message", [layout] {
|
||||
menu.addAction("Copy full message", [layout] {
|
||||
QString copyString;
|
||||
layout->addSelectionText(copyString);
|
||||
|
||||
crossPlatformCopy(copyString);
|
||||
});
|
||||
|
||||
// If is a link to a Twitch user/stream
|
||||
if (hoveredElement->getLink().type == Link::Url)
|
||||
{
|
||||
static QRegularExpression twitchChannelRegex(
|
||||
R"(^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/(?:popout\/)?(?<username>[a-z0-9_]{3,}))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static QSet<QString> ignoredUsernames{
|
||||
"directory", //
|
||||
"downloads", //
|
||||
"drops", //
|
||||
"friends", //
|
||||
"inventory", //
|
||||
"jobs", //
|
||||
"login", //
|
||||
"messages", //
|
||||
"payments", //
|
||||
"profile", //
|
||||
"security", //
|
||||
"settings", //
|
||||
"signup", //
|
||||
"subscriptions", //
|
||||
"turbo", //
|
||||
"videos", //
|
||||
"wallet", //
|
||||
};
|
||||
|
||||
auto twitchMatch =
|
||||
twitchChannelRegex.match(hoveredElement->getLink().value);
|
||||
auto twitchUsername = twitchMatch.captured("username");
|
||||
if (!twitchUsername.isEmpty() &&
|
||||
!ignoredUsernames.contains(twitchUsername))
|
||||
{
|
||||
menu->addSeparator();
|
||||
menu->addAction("Open in new split", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(twitchUsername,
|
||||
FromTwitchLinkOpenChannelIn::Split);
|
||||
});
|
||||
menu->addAction("Open in new tab", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(twitchUsername,
|
||||
FromTwitchLinkOpenChannelIn::Tab);
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
menu->addAction("Open player in browser", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(
|
||||
twitchUsername, FromTwitchLinkOpenChannelIn::BrowserPlayer);
|
||||
});
|
||||
menu->addAction("Open in streamlink", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(
|
||||
twitchUsername, FromTwitchLinkOpenChannelIn::Streamlink);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu->popup(QCursor::pos());
|
||||
menu->raise();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void ChannelView::addTwitchLinkContextMenuItems(
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr /*layout*/,
|
||||
QMouseEvent * /*event*/, QMenu &menu)
|
||||
{
|
||||
if (hoveredElement == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &link = hoveredElement->getLink();
|
||||
|
||||
if (link.type != Link::Url)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
static QRegularExpression twitchChannelRegex(
|
||||
R"(^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/(?:popout\/)?(?<username>[a-z0-9_]{3,}))",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
static QSet<QString> ignoredUsernames{
|
||||
"directory", //
|
||||
"downloads", //
|
||||
"drops", //
|
||||
"friends", //
|
||||
"inventory", //
|
||||
"jobs", //
|
||||
"login", //
|
||||
"messages", //
|
||||
"payments", //
|
||||
"profile", //
|
||||
"security", //
|
||||
"settings", //
|
||||
"signup", //
|
||||
"subscriptions", //
|
||||
"turbo", //
|
||||
"videos", //
|
||||
"wallet", //
|
||||
};
|
||||
|
||||
auto twitchMatch = twitchChannelRegex.match(link.value);
|
||||
auto twitchUsername = twitchMatch.captured("username");
|
||||
if (!twitchUsername.isEmpty() && !ignoredUsernames.contains(twitchUsername))
|
||||
{
|
||||
menu.addSeparator();
|
||||
menu.addAction("Open in new split", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(twitchUsername,
|
||||
FromTwitchLinkOpenChannelIn::Split);
|
||||
});
|
||||
menu.addAction("Open in new tab", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(twitchUsername,
|
||||
FromTwitchLinkOpenChannelIn::Tab);
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
menu.addAction("Open player in browser", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(
|
||||
twitchUsername, FromTwitchLinkOpenChannelIn::BrowserPlayer);
|
||||
});
|
||||
menu.addAction("Open in streamlink", [twitchUsername, this] {
|
||||
this->openChannelIn.invoke(twitchUsername,
|
||||
FromTwitchLinkOpenChannelIn::Streamlink);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ChannelView::addHiddenContextMenuItems(
|
||||
const MessageLayoutElement * /*hoveredElement*/, MessageLayoutPtr layout,
|
||||
QMouseEvent *event, QMenu &menu)
|
||||
{
|
||||
if (!layout)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->modifiers() != Qt::ShiftModifier)
|
||||
{
|
||||
// NOTE: We currently require the modifier to be ONLY shift - we might want to check if shift is among the modifiers instead
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layout->getMessage()->id.isEmpty())
|
||||
{
|
||||
menu.addAction("Copy message ID",
|
||||
[messageID = layout->getMessage()->id] {
|
||||
crossPlatformCopy(messageID);
|
||||
});
|
||||
}
|
||||
}
|
||||
void ChannelView::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<MessageLayout> layout;
|
||||
QPoint relativePos;
|
||||
int messageIndex;
|
||||
|
@ -2060,12 +2155,8 @@ void ChannelView::hideEvent(QHideEvent *)
|
|||
|
||||
void ChannelView::showUserInfoPopup(const QString &userName)
|
||||
{
|
||||
QWidget *userCardParent = this;
|
||||
#ifdef Q_OS_MACOS
|
||||
// Order of closing/opening/killing widgets when the "Automatically close user info popups" setting is enabled is special on macOS, so user info popups should always use the main window as its parent
|
||||
userCardParent =
|
||||
QWidget *userCardParent =
|
||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow()));
|
||||
#endif
|
||||
auto *userPopup =
|
||||
new UserInfoPopup(getSettings()->autoCloseUserPopup, userCardParent);
|
||||
userPopup->setData(userName, this->hasSourceChannel()
|
||||
|
|
|
@ -157,10 +157,25 @@ private:
|
|||
const QPoint &relativePos, int &wordStart, int &wordEnd);
|
||||
|
||||
void handleMouseClick(QMouseEvent *event,
|
||||
const MessageLayoutElement *hoverLayoutElement,
|
||||
const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout);
|
||||
void addContextMenuItems(const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout);
|
||||
MessageLayoutPtr layout, QMouseEvent *event);
|
||||
void addImageContextMenuItems(const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout, QMouseEvent *event,
|
||||
QMenu &menu);
|
||||
void addLinkContextMenuItems(const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout, QMouseEvent *event,
|
||||
QMenu &menu);
|
||||
void addMessageContextMenuItems(const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout, QMouseEvent *event,
|
||||
QMenu &menu);
|
||||
void addTwitchLinkContextMenuItems(
|
||||
const MessageLayoutElement *hoveredElement, MessageLayoutPtr layout,
|
||||
QMouseEvent *event, QMenu &menu);
|
||||
void addHiddenContextMenuItems(const MessageLayoutElement *hoveredElement,
|
||||
MessageLayoutPtr layout, QMouseEvent *event,
|
||||
QMenu &menu);
|
||||
int getLayoutWidth() const;
|
||||
void updatePauses();
|
||||
void unpaused();
|
||||
|
|
|
@ -151,20 +151,13 @@ void SearchPopup::initLayout()
|
|||
{
|
||||
this->searchInput_ = new QLineEdit(this);
|
||||
layout2->addWidget(this->searchInput_);
|
||||
QObject::connect(this->searchInput_, &QLineEdit::returnPressed,
|
||||
[this] {
|
||||
this->search();
|
||||
});
|
||||
}
|
||||
|
||||
// SEARCH BUTTON
|
||||
{
|
||||
QPushButton *searchButton = new QPushButton(this);
|
||||
searchButton->setText("Search");
|
||||
layout2->addWidget(searchButton);
|
||||
QObject::connect(searchButton, &QPushButton::clicked, [this] {
|
||||
this->search();
|
||||
});
|
||||
this->searchInput_->setPlaceholderText("Type to search");
|
||||
this->searchInput_->setClearButtonEnabled(true);
|
||||
this->searchInput_->findChild<QAbstractButton *>()->setIcon(
|
||||
QPixmap(":/buttons/clearSearch.png"));
|
||||
QObject::connect(this->searchInput_, &QLineEdit::textChanged,
|
||||
this, &SearchPopup::search);
|
||||
}
|
||||
|
||||
layout1->addLayout(layout2);
|
||||
|
|
|
@ -144,30 +144,34 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
|||
[](auto args) {
|
||||
return fuzzyToFloat(args.value, 1.f);
|
||||
});
|
||||
layout.addDropdown<int>(
|
||||
"Tab layout", {"Horizontal", "Vertical"}, s.tabDirection,
|
||||
[](auto val) {
|
||||
switch (val)
|
||||
{
|
||||
case NotebookTabDirection::Horizontal:
|
||||
return "Horizontal";
|
||||
case NotebookTabDirection::Vertical:
|
||||
return "Vertical";
|
||||
}
|
||||
ComboBox *tabDirectionDropdown =
|
||||
layout.addDropdown<std::underlying_type<NotebookTabDirection>::type>(
|
||||
"Tab layout", {"Horizontal", "Vertical"}, s.tabDirection,
|
||||
[](auto val) {
|
||||
switch (val)
|
||||
{
|
||||
case NotebookTabDirection::Horizontal:
|
||||
return "Horizontal";
|
||||
case NotebookTabDirection::Vertical:
|
||||
return "Vertical";
|
||||
}
|
||||
|
||||
return "";
|
||||
},
|
||||
[](auto args) {
|
||||
if (args.value == "Vertical")
|
||||
{
|
||||
return NotebookTabDirection::Vertical;
|
||||
}
|
||||
else
|
||||
{
|
||||
// default to horizontal
|
||||
return NotebookTabDirection::Horizontal;
|
||||
}
|
||||
});
|
||||
return "";
|
||||
},
|
||||
[](auto args) {
|
||||
if (args.value == "Vertical")
|
||||
{
|
||||
return NotebookTabDirection::Vertical;
|
||||
}
|
||||
else
|
||||
{
|
||||
// default to horizontal
|
||||
return NotebookTabDirection::Horizontal;
|
||||
}
|
||||
},
|
||||
false);
|
||||
tabDirectionDropdown->setMinimumWidth(
|
||||
tabDirectionDropdown->minimumSizeHint().width());
|
||||
|
||||
layout.addCheckbox("Show tab close button", s.showTabCloseButton);
|
||||
layout.addCheckbox("Always on top", s.windowTopMost);
|
||||
|
|
|
@ -106,9 +106,9 @@ void InputCompletionPopup::updateEmotes(const QString &text, ChannelPtr channel)
|
|||
addEmotes(emotes, *ffz, text, "Channel FrankerFaceZ");
|
||||
}
|
||||
|
||||
if (auto bttvG = getApp()->twitch2->getBttvEmotes().emotes())
|
||||
if (auto bttvG = getApp()->twitch->getBttvEmotes().emotes())
|
||||
addEmotes(emotes, *bttvG, text, "Global BetterTTV");
|
||||
if (auto ffzG = getApp()->twitch2->getFfzEmotes().emotes())
|
||||
if (auto ffzG = getApp()->twitch->getFfzEmotes().emotes())
|
||||
addEmotes(emotes, *ffzG, text, "Global FrankerFaceZ");
|
||||
|
||||
addEmojis(emotes, getApp()->emotes->emojis.emojis, text);
|
||||
|
|
|
@ -125,8 +125,7 @@ Split::Split(QWidget *parent)
|
|||
this->view_->openChannelIn.connect([this](
|
||||
QString twitchChannel,
|
||||
FromTwitchLinkOpenChannelIn openIn) {
|
||||
ChannelPtr channel =
|
||||
getApp()->twitch.server->getOrAddChannel(twitchChannel);
|
||||
ChannelPtr channel = getApp()->twitch->getOrAddChannel(twitchChannel);
|
||||
switch (openIn)
|
||||
{
|
||||
case FromTwitchLinkOpenChannelIn::Split:
|
||||
|
|
|
@ -785,11 +785,19 @@ void SplitHeader::paintEvent(QPaintEvent *)
|
|||
{
|
||||
QPainter painter(this);
|
||||
|
||||
painter.fillRect(rect(), this->theme->splits.header.background);
|
||||
painter.setPen(this->theme->splits.header.border);
|
||||
QColor background = this->theme->splits.header.background;
|
||||
QColor border = this->theme->splits.header.border;
|
||||
|
||||
if (this->split_->hasFocus())
|
||||
{
|
||||
background = this->theme->splits.header.focusedBackground;
|
||||
border = this->theme->splits.header.focusedBorder;
|
||||
}
|
||||
|
||||
painter.fillRect(rect(), background);
|
||||
painter.setPen(border);
|
||||
painter.drawRect(0, 0, width() - 1, height() - 2);
|
||||
painter.fillRect(0, height() - 1, width(), 1,
|
||||
this->theme->splits.background);
|
||||
painter.fillRect(0, height() - 1, width(), 1, background);
|
||||
}
|
||||
|
||||
void SplitHeader::mousePressEvent(QMouseEvent *event)
|
||||
|
@ -909,6 +917,8 @@ void SplitHeader::themeChangedEvent()
|
|||
this->dropdownButton_->setPixmap(getResources().buttons.menuLight);
|
||||
this->addButton_->setPixmap(getResources().buttons.addSplitDark);
|
||||
}
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
void SplitHeader::reloadChannelEmotes()
|
||||
|
|
|
@ -154,10 +154,10 @@ void SplitInput::themeChangedEvent()
|
|||
this->updateEmoteButton();
|
||||
this->ui_.textEditLength->setPalette(palette);
|
||||
|
||||
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
|
||||
this->ui_.textEdit->setPalette(placeholderPalette);
|
||||
#endif
|
||||
this->ui_.textEdit->setStyleSheet(this->theme->splits.input.styleSheet);
|
||||
|
||||
this->ui_.hbox->setMargin(
|
||||
int((this->theme->isLightTheme() ? 4 : 2) * this->scale()));
|
||||
|
@ -687,7 +687,7 @@ void SplitInput::editTextChanged()
|
|||
if (text.startsWith("/r ", Qt::CaseInsensitive) &&
|
||||
this->split_->getChannel()->isTwitchChannel())
|
||||
{
|
||||
QString lastUser = app->twitch.server->lastUserThatWhisperedMe.get();
|
||||
QString lastUser = app->twitch->lastUserThatWhisperedMe.get();
|
||||
if (!lastUser.isEmpty())
|
||||
{
|
||||
this->ui_.textEdit->setPlainText("/w " + lastUser + text.mid(2));
|
||||
|
|
|
@ -14,6 +14,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/RatelimitBucket.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Hotkeys.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/UtilTwitch.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
||||
|
|
161
tests/src/UtilTwitch.cpp
Normal file
161
tests/src/UtilTwitch.cpp
Normal file
|
@ -0,0 +1,161 @@
|
|||
#include "util/Twitch.hpp"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
TEST(UtilTwitch, StripUserName)
|
||||
{
|
||||
struct TestCase {
|
||||
QString inputUserName;
|
||||
QString expectedUserName;
|
||||
};
|
||||
|
||||
std::vector<TestCase> tests{
|
||||
{
|
||||
"pajlada",
|
||||
"pajlada",
|
||||
},
|
||||
{
|
||||
"Pajlada",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"@Pajlada",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"@Pajlada,",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"@@Pajlada,",
|
||||
"@Pajlada",
|
||||
},
|
||||
{
|
||||
"@@Pajlada,,",
|
||||
"@Pajlada,",
|
||||
},
|
||||
{
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"@",
|
||||
"",
|
||||
},
|
||||
{
|
||||
",",
|
||||
"",
|
||||
},
|
||||
{
|
||||
// We purposefully don't handle spaces at the end, as all expected usages of this function split the message up by space and strip the parameters by themselves
|
||||
", ",
|
||||
", ",
|
||||
},
|
||||
{
|
||||
// We purposefully don't handle spaces at the start, as all expected usages of this function split the message up by space and strip the parameters by themselves
|
||||
" @",
|
||||
" @",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto &[inputUserName, expectedUserName] : tests)
|
||||
{
|
||||
QString userName = inputUserName;
|
||||
stripUserName(userName);
|
||||
|
||||
EXPECT_EQ(userName, expectedUserName)
|
||||
<< qUtf8Printable(userName) << " (" << qUtf8Printable(inputUserName)
|
||||
<< ") did not match expected value "
|
||||
<< qUtf8Printable(expectedUserName);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(UtilTwitch, StripChannelName)
|
||||
{
|
||||
struct TestCase {
|
||||
QString inputChannelName;
|
||||
QString expectedChannelName;
|
||||
};
|
||||
|
||||
std::vector<TestCase> tests{
|
||||
{
|
||||
"pajlada",
|
||||
"pajlada",
|
||||
},
|
||||
{
|
||||
"Pajlada",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"@Pajlada",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"#Pajlada",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"#Pajlada,",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"#Pajlada,",
|
||||
"Pajlada",
|
||||
},
|
||||
{
|
||||
"@@Pajlada,",
|
||||
"@Pajlada",
|
||||
},
|
||||
{
|
||||
// We only strip one character off the front
|
||||
"#@Pajlada,",
|
||||
"@Pajlada",
|
||||
},
|
||||
{
|
||||
"@@Pajlada,,",
|
||||
"@Pajlada,",
|
||||
},
|
||||
{
|
||||
"",
|
||||
"",
|
||||
},
|
||||
{
|
||||
"@",
|
||||
"",
|
||||
},
|
||||
{
|
||||
",",
|
||||
"",
|
||||
},
|
||||
{
|
||||
// We purposefully don't handle spaces at the end, as all expected usages of this function split the message up by space and strip the parameters by themselves
|
||||
", ",
|
||||
", ",
|
||||
},
|
||||
{
|
||||
// We purposefully don't handle spaces at the start, as all expected usages of this function split the message up by space and strip the parameters by themselves
|
||||
" #",
|
||||
" #",
|
||||
},
|
||||
};
|
||||
|
||||
for (const auto &[inputChannelName, expectedChannelName] : tests)
|
||||
{
|
||||
QString userName = inputChannelName;
|
||||
stripChannelName(userName);
|
||||
|
||||
EXPECT_EQ(userName, expectedChannelName)
|
||||
<< qUtf8Printable(userName) << " ("
|
||||
<< qUtf8Printable(inputChannelName)
|
||||
<< ") did not match expected value "
|
||||
<< qUtf8Printable(expectedChannelName);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue