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:
James Upjohn 2022-03-31 07:20:18 +13:00 committed by GitHub
parent 953af4d254
commit d2b89f028b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1285 additions and 980 deletions

View file

@ -2,10 +2,7 @@
name: Build on Linux
on:
push:
branches:
- master
pull_request:
workflow_call:
jobs:
build:
@ -33,17 +30,15 @@ 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: |
@ -67,14 +62,13 @@ jobs:
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
make -j$(nproc)
shell: bash
- name: Build with CMake (Ubuntu)
@ -88,7 +82,7 @@ jobs:
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \
-DUSE_PRECOMPILED_HEADERS=${{ matrix.pch }} \
..
make -j8
make -j$(nproc)
shell: bash
- name: Package - AppImage (Ubuntu)
@ -104,13 +98,13 @@ jobs:
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
View 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

View 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

View file

@ -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
@ -186,42 +39,42 @@ jobs:
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/

View file

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

View file

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

View file

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

1
.gitignore vendored
View file

@ -81,6 +81,7 @@ Thumbs.db
.vscode
.idea
dependencies
.cache
### CMake ###
CMakeLists.txt.user

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
[requires]
openssl/1.1.1k
boost/1.76.0
openssl/1.1.1m
boost/1.78.0
[generators]
qmake

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

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

View file

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

View 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,16 +380,18 @@ void Application::initPubsub()
});
});
this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) {
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);
auto chan = this->twitch->getChannelOrEmptyByID(channelId);
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (auto channel = dynamic_cast<TwitchChannel *>(chan.get()))
if (auto channel =
dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
@ -414,14 +404,14 @@ void Application::initPubsub()
}
});
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());
};

View file

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

View file

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

View file

@ -79,6 +79,8 @@ namespace {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QApplication::setWindowIcon(QIcon(":/icon.ico"));
installCustomPalette();
}

View file

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

View file

@ -11,8 +11,10 @@ public:
struct {
QPixmap _1xelerate;
QPixmap alazymeme;
QPixmap brian6932;
QPixmap fourtf;
QPixmap kararty;
QPixmap karlpolice;
QPixmap mm2pl;
QPixmap pajlada;
QPixmap slch;

View file

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

View file

@ -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);
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,6 +675,7 @@ void CommandController::initialize(Settings &, Paths &paths)
"popout chat."));
return "";
}
}
stripChannelName(target);
QDesktopServices::openUrl(
@ -699,6 +685,51 @@ void CommandController::initialize(Settings &, Paths &paths)
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) {
auto *currentPage = dynamic_cast<SplitContainer *>(
@ -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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,6 +739,8 @@ void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
{
status->gameId = stream.gameId;
if (!stream.gameId.isEmpty())
{
// Resolve game ID to game name
getHelix()->getGameById(
stream.gameId,
@ -761,6 +762,12 @@ void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
// 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);
auto diff = since.secsTo(QDateTime::currentDateTime());
@ -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()

View file

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

View file

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

View file

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

View file

@ -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,8 +10,8 @@ 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
@ -34,16 +21,16 @@ URL: https://dev.twitch.tv/docs/api/reference#get-users
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
### 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
@ -52,16 +39,16 @@ URL: https://dev.twitch.tv/docs/api/reference#get-streams
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
### 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
### Update Channel
@ -69,8 +56,8 @@ URL: https://dev.twitch.tv/docs/api/reference#get-channel-information
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
@ -79,8 +66,8 @@ Requires `channel:manage:broadcast` scope
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
### Get User Block List
@ -88,8 +75,8 @@ Requires `user:edit:broadcast` scope
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
### Block User
@ -97,8 +84,8 @@ Requires `user:read:blocked_users` scope
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
@ -107,8 +94,8 @@ Requires `user:manage:blocked_users` scope
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
@ -116,8 +103,8 @@ Requires `user:manage:blocked_users` scope
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
### Manage Held AutoMod Messages
@ -125,31 +112,28 @@ URL: https://dev.twitch.tv/docs/api/reference#search-categories
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
### 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`
### 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.
## TMI

View file

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

View file

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

View file

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

View file

@ -31,7 +31,9 @@ public:
struct {
QColor border;
QColor focusedBorder;
QColor background;
QColor focusedBackground;
QColor text;
QColor focusedText;
// int margin;

View file

@ -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")
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -130,8 +130,6 @@ float BaseWindow::qtFontScale() const
void BaseWindow::init()
{
this->setWindowIcon(QIcon(":/images/icon.png"));
#ifdef USEWINSDK
if (this->hasCustomWindowFrame())
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 (hoveredElement != nullptr)
{
const auto &link = hoveredElement->getLink();
if (link.type == Link::UserInfo)
{
const bool commaMention = getSettings()->mentionUsersWithComma;
const bool commaMention =
getSettings()->mentionUsersWithComma;
const bool isFirstWord =
split && split->getInput().isEditFirstWord();
auto userMention =
formatUserMention(link.value, isFirstWord, commaMention);
auto userMention = formatUserMention(
link.value, isFirstWord, commaMention);
insertText("@" + userMention + " ");
return;
}
else if (link.type == Link::UserWhisper)
if (link.type == Link::UserWhisper)
{
insertText("/w " + link.value + " ");
return;
}
else
{
this->addContextMenuItems(hoveredElement, layout);
}
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 = hoveredElement->getLink().value;
QString url = link.value;
// open link
menu->addAction("Open link", [url] {
menu.addAction("Open link", [url] {
QDesktopServices::openUrl(QUrl(url));
});
// open link default
if (supportsIncognitoLinks())
{
menu->addAction("Open link incognito", [url] {
menu.addAction("Open link incognito", [url] {
openLinkIncognito(url);
});
}
menu->addAction("Copy link", [url] {
menu.addAction("Copy link", [url] {
crossPlatformCopy(url);
});
menu->addSeparator();
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,16 +1983,30 @@ 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)
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);
@ -1955,42 +2030,62 @@ void ChannelView::addContextMenuItems(
"wallet", //
};
auto twitchMatch =
twitchChannelRegex.match(hoveredElement->getLink().value);
auto twitchMatch = twitchChannelRegex.match(link.value);
auto twitchUsername = twitchMatch.captured("username");
if (!twitchUsername.isEmpty() &&
!ignoredUsernames.contains(twitchUsername))
if (!twitchUsername.isEmpty() && !ignoredUsernames.contains(twitchUsername))
{
menu->addSeparator();
menu->addAction("Open in new split", [twitchUsername, this] {
menu.addSeparator();
menu.addAction("Open in new split", [twitchUsername, this] {
this->openChannelIn.invoke(twitchUsername,
FromTwitchLinkOpenChannelIn::Split);
});
menu->addAction("Open in new tab", [twitchUsername, this] {
menu.addAction("Open in new tab", [twitchUsername, this] {
this->openChannelIn.invoke(twitchUsername,
FromTwitchLinkOpenChannelIn::Tab);
});
menu->addSeparator();
menu->addAction("Open player in browser", [twitchUsername, this] {
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.addAction("Open in streamlink", [twitchUsername, this] {
this->openChannelIn.invoke(twitchUsername,
FromTwitchLinkOpenChannelIn::Streamlink);
});
}
}
menu->popup(QCursor::pos());
menu->raise();
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()

View file

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

View file

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

View file

@ -144,7 +144,8 @@ void GeneralPage::initLayout(GeneralPageView &layout)
[](auto args) {
return fuzzyToFloat(args.value, 1.f);
});
layout.addDropdown<int>(
ComboBox *tabDirectionDropdown =
layout.addDropdown<std::underlying_type<NotebookTabDirection>::type>(
"Tab layout", {"Horizontal", "Vertical"}, s.tabDirection,
[](auto val) {
switch (val)
@ -167,7 +168,10 @@ void GeneralPage::initLayout(GeneralPageView &layout)
// 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);

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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);
}
}