Merge branch 'master' of github.com:Chatterino/chatterino2 into feature/image_uploader_ui

This commit is contained in:
Mm2PL 2024-05-14 00:49:05 +02:00
commit 847a300e36
No known key found for this signature in database
GPG key ID: 94AC9B80EFA15ED9
185 changed files with 4856 additions and 1714 deletions

View file

@ -2,7 +2,7 @@
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#define MyAppName "Chatterino"
#define MyAppVersion "2.4.6"
#define MyAppVersion "2.5.1"
#define MyAppPublisher "Chatterino Team"
#define MyAppURL "https://www.chatterino.com"
#define MyAppExeName "chatterino.exe"

34
.CI/setup-clang-tidy.sh Executable file
View file

@ -0,0 +1,34 @@
#!/bin/bash
set -ev;
# aqt installs into .qtinstall/Qt/<version>/gcc_64
# This is doing the same as jurplel/install-qt-action
# See https://github.com/jurplel/install-qt-action/blob/74ca8cd6681420fc8894aed264644c7a76d7c8cb/action/src/main.ts#L52-L74
qtpath=$(echo .qtinstall/Qt/[0-9]*/*/bin/qmake | sed -e s:/bin/qmake$::)
export LD_LIBRARY_PATH="$qtpath/lib"
export QT_ROOT_DIR=$qtpath
export QT_PLUGIN_PATH="$qtpath/plugins"
export PATH="$PATH:$(realpath "$qtpath/bin")"
export Qt6_DIR="$(realpath "$qtpath")"
cmake -S. -Bbuild-clang-tidy \
-DCMAKE_BUILD_TYPE=Debug \
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \
-DUSE_PRECOMPILED_HEADERS=OFF \
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
-DCHATTERINO_LTO=Off \
-DCHATTERINO_PLUGINS=On \
-DBUILD_WITH_QT6=On \
-DBUILD_TESTS=On \
-DBUILD_BENCHMARKS=On
# Run MOC and UIC
# This will compile the dependencies
# Get the targets using `ninja -t targets | grep autogen`
cmake --build build-clang-tidy --parallel -t \
Core_autogen \
LibCommuni_autogen \
Model_autogen \
Util_autogen \
chatterino-lib_autogen

View file

@ -50,3 +50,4 @@ PointerBindsToType: false
SpacesBeforeTrailingComments: 2
Standard: Auto
ReflowComments: false
InsertNewlineAtEOF: true

View file

@ -112,24 +112,24 @@ jobs:
matrix:
include:
# macOS
- os: macos-latest
- os: macos-13
qt-version: 5.15.2
force-lto: false
plugins: false
plugins: true
skip-artifact: false
skip-crashpad: false
# Windows
- os: windows-latest
qt-version: 6.5.0
force-lto: false
plugins: false
plugins: true
skip-artifact: false
skip-crashpad: false
# Windows 7/8
- os: windows-latest
qt-version: 5.15.2
force-lto: false
plugins: false
plugins: true
skip-artifact: false
skip-crashpad: true
@ -139,6 +139,8 @@ jobs:
C2_PLUGINS: ${{ matrix.plugins }}
C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }}
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }}
C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }}
C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }}
steps:
- uses: actions/checkout@v4
@ -188,16 +190,9 @@ jobs:
if: startsWith(matrix.os, 'windows')
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables (Windows)
if: startsWith(matrix.os, 'windows')
run: |
"C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV"
"C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV"
shell: powershell
- name: Setup sccache (Windows)
# sccache v0.7.4
uses: hendrikmuhs/ccache-action@v1.2.12
uses: hendrikmuhs/ccache-action@v1.2.13
if: startsWith(matrix.os, 'windows')
with:
variant: sccache

View file

@ -26,7 +26,7 @@ jobs:
run: sudo apt-get -y install dos2unix
- name: Check formatting
uses: DoozyX/clang-format-lint-action@v0.16.2
uses: DoozyX/clang-format-lint-action@v0.17
with:
source: "./src ./tests/src ./benchmarks/src ./mocks/include"
extensions: "hpp,cpp"

View file

@ -8,60 +8,25 @@ concurrency:
group: clang-tidy-${{ github.ref }}
cancel-in-progress: true
env:
CHATTERINO_REQUIRE_CLEAN_GIT: On
C2_BUILD_WITH_QT6: Off
jobs:
build:
review:
name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})"
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
# Ubuntu 22.04, Qt 5.15
# Ubuntu 22.04, Qt 6.6
- os: ubuntu-22.04
qt-version: 5.15.2
plugins: false
qt-version: 6.6.2
fail-fast: false
steps:
- name: Enable plugin support
if: matrix.plugins
run: |
echo "C2_PLUGINS=ON" >> "$GITHUB_ENV"
shell: bash
- name: Set BUILD_WITH_QT6
if: startsWith(matrix.qt-version, '6.')
run: |
echo "C2_BUILD_WITH_QT6=ON" >> "$GITHUB_ENV"
shell: bash
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0 # allows for tags access
- name: Install Qt5
if: startsWith(matrix.qt-version, '5.')
uses: jurplel/install-qt-action@v3.3.0
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
version: ${{ matrix.qt-version }}
- name: Install Qt 6.5.3 imageformats
if: startsWith(matrix.qt-version, '6.')
uses: jurplel/install-qt-action@v3.3.0
with:
cache: false
modules: qtimageformats
set-env: false
version: 6.5.3
extra: --noarchives
- name: Install Qt6
if: startsWith(matrix.qt-version, '6.')
uses: jurplel/install-qt-action@v3.3.0
@ -70,79 +35,31 @@ jobs:
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
modules: qt5compat qtimageformats
version: ${{ matrix.qt-version }}
# LINUX
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get -y install \
cmake \
virtualenv \
rapidjson-dev \
libfuse2 \
libssl-dev \
libboost-dev \
libxcb-randr0-dev \
libboost-system-dev \
libboost-filesystem-dev \
libpulse-dev \
libxkbcommon-x11-0 \
build-essential \
libgl1-mesa-dev \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-render-util0 \
libxcb-xinerama0
- name: Apply Qt5 patches
if: startsWith(matrix.qt-version, '5.')
run: |
patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch
shell: bash
- name: Build
run: |
mkdir build
cd build
CXXFLAGS=-fno-sized-deallocation cmake \
-DCMAKE_INSTALL_PREFIX=appdir/usr/ \
-DCMAKE_BUILD_TYPE=Release \
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \
-DUSE_PRECOMPILED_HEADERS=OFF \
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
-DCHATTERINO_LTO="$C2_ENABLE_LTO" \
-DCHATTERINO_PLUGINS="$C2_PLUGINS" \
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
..
shell: bash
dir: ${{ github.workspace }}/.qtinstall
set-env: false
- name: clang-tidy review
timeout-minutes: 20
uses: ZedThree/clang-tidy-review@v0.17.1
uses: ZedThree/clang-tidy-review@v0.18.0
with:
build_dir: build-clang-tidy
config_file: ".clang-tidy"
split_workflow: true
exclude: "lib/*,tools/crash-handler/*"
cmake_command: >-
cmake -S. -Bbuild-clang-tidy
-DCMAKE_BUILD_TYPE=Release
-DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On
-DUSE_PRECOMPILED_HEADERS=OFF
-DCMAKE_EXPORT_COMPILE_COMMANDS=On
-DCHATTERINO_LTO=Off
-DCHATTERINO_PLUGINS=On
-DBUILD_WITH_QT6=Off
-DBUILD_TESTS=On
-DBUILD_BENCHMARKS=On
./.CI/setup-clang-tidy.sh
apt_packages: >-
qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev,
libsecret-1-dev,
libboost-dev, libboost-system-dev, libboost-filesystem-dev,
libssl-dev,
rapidjson-dev,
libbenchmark-dev
libbenchmark-dev,
build-essential,
libgl1-mesa-dev, libgstreamer-gl1.0-0, libpulse-dev,
libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0,
libxcb-render-util0, libxcb-render0, libxcb-shape0, libxcb-shm0, libxcb-sync1,
libxcb-util1, libxcb-xfixes0, libxcb-xinerama0, libxcb1, libxkbcommon-dev,
libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0
- name: clang-tidy-review upload
uses: ZedThree/clang-tidy-review/upload@v0.17.1
uses: ZedThree/clang-tidy-review/upload@v0.18.0

View file

@ -26,4 +26,5 @@ jobs:
echo "Running bump-cask-pr for cask '$C2_CASK_NAME' and version '$C2_TAGGED_VERSION'"
C2_TAGGED_VERSION_STRIPPED="${C2_TAGGED_VERSION:1}"
echo "Stripped version: '$C2_TAGGED_VERSION_STRIPPED'"
brew developer on
brew bump-cask-pr --version "$C2_TAGGED_VERSION_STRIPPED" "$C2_CASK_NAME"

View file

@ -8,12 +8,13 @@ on:
- completed
jobs:
build:
post:
runs-on: ubuntu-latest
# Only when a build succeeds
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: ZedThree/clang-tidy-review/post@v0.17.1
- uses: ZedThree/clang-tidy-review/post@v0.18.0
with:
lgtm_comment_body: ""
num_comments_as_exitcode: false

View file

@ -32,6 +32,8 @@ jobs:
env:
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }}
C2_USE_OPENSSL3: ${{ startsWith(matrix.qt-version, '6.') && 'True' || 'False' }}
C2_CONAN_CACHE_SUFFIX: ${{ startsWith(matrix.qt-version, '6.') && '-QT6' || '' }}
steps:
- name: Enable plugin support
@ -65,15 +67,9 @@ jobs:
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables
if: startsWith(matrix.os, 'windows')
run: |
"C2_USE_OPENSSL3=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "True" } else { "False" })" >> "$Env:GITHUB_ENV"
"C2_CONAN_CACHE_SUFFIX=$(if ($Env:C2_BUILD_WITH_QT6 -eq "on") { "-QT6" } else { "`" })" >> "$Env:GITHUB_ENV"
- name: Setup sccache
# sccache v0.7.4
uses: hendrikmuhs/ccache-action@v1.2.12
uses: hendrikmuhs/ccache-action@v1.2.13
with:
variant: sccache
# only save on the default (master) branch

14
.github/workflows/winget.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest
if: ${{ startsWith(github.event.release.tag_name, 'v') }}
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: ChatterinoTeam.Chatterino
installers-regex: ^Chatterino.Installer.exe$
token: ${{ secrets.WINGET_TOKEN }}

View file

@ -15,7 +15,7 @@ FreeBSD 13.0-CURRENT.
mkdir build
cd build
```
1. Generate build files
1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command.
```sh
cmake ..
```

View file

@ -1,38 +1,49 @@
# Linux
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**.
For all dependencies below we use Qt6. Our minimum supported version is Qt5.15, but you are on your own.
## Install dependencies
### Ubuntu 20.04
### Ubuntu
_Most likely works the same for other Debian-like distros._
Building on Ubuntu requires Docker.
Install all the dependencies using `sudo apt install qttools5-dev qt5-image-formats-plugins libqt5svg5-dev libboost-dev libssl-dev libboost-system-dev libboost-filesystem-dev cmake g++ libsecret-1-dev`
Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-20.04 as your base if you're on Ubuntu 20.04.
Use https://github.com/Chatterino/docker/pkgs/container/chatterino2-build-ubuntu-22.04 if you're on Ubuntu 22.04.
The built binary should be exportable from the final image & able to run on your system assuming you perform a static build. See our [build.yml github workflow file](.github/workflows/build.yml) for the cmake line used for Ubuntu builds.
### Debian 12 (bookworm) or later
```sh
sudo apt install qt6-base-dev qt6-5compat-dev qt6-svg-dev qt6-image-formats-plugins libboost1.81-dev libssl-dev cmake g++ git
```
### Arch Linux
Install all the dependencies using `sudo pacman -S --needed qt5-base qt5-imageformats qt5-svg qt5-tools boost rapidjson pkgconf openssl cmake`
```sh
sudo pacman -S --needed qt6-base qt6-tools boost-libs openssl qt6-imageformats qt6-5compat qt6-svg boost rapidjson pkgconf openssl cmake
```
Alternatively you can use the [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) package to build and install Chatterino for you.
### Fedora 28 and above
### Fedora 39 and above
_Most likely works the same for other Red Hat-like distros. Substitute `dnf` with `yum`._
Install all the dependencies using `sudo dnf install qt5-qtbase-devel qt5-qtimageformats qt5-qtsvg-devel qt5-linguist libsecret-devel openssl-devel boost-devel cmake`
```sh
sudo dnf install qt6-qtbase-devel qt6-qtimageformats qt6-qtsvg-devel qt6-qt5compat-devel g++ git openssl-devel boost-devel cmake
```
### NixOS 18.09+
Enter the development environment with all the dependencies: `nix-shell -p openssl boost qt5.full pkg-config cmake`
```sh
nix-shell -p openssl boost qt6.full pkg-config cmake
```
## Compile
### Through Qt Creator
1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator`
1. Open `CMakeLists.txt` with Qt Creator and select build
## Manually
1. In the project directory, create a build directory and enter it
@ -40,11 +51,16 @@ Enter the development environment with all the dependencies: `nix-shell -p opens
mkdir build
cd build
```
1. Generate build files
1. Generate build files. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command.
```sh
cmake ..
cmake -DBUILD_WITH_QT6=ON -DBUILD_WITH_QTKEYCHAIN=OFF ..
```
1. Build the project
```sh
make
cmake --build .
```
### Through Qt Creator
1. Install C++ IDE Qt Creator by using `sudo apt install qtcreator` (Or whatever equivalent for your distro)
1. Open `CMakeLists.txt` with Qt Creator and select build

View file

@ -20,7 +20,7 @@ Local dev machines for testing are available on Apple Silicon on macOS 13.
1. Go to the project directory where you cloned Chatterino2 & its submodules
1. Create a build directory and go into it:
`mkdir build && cd build`
1. Run CMake:
1. Run CMake. To enable Lua plugins in your build add `-DCHATTERINO_PLUGINS=ON` to this command.
`cmake -DCMAKE_PREFIX_PATH=/opt/homebrew/opt/qt@5 -DOPENSSL_ROOT_DIR=/opt/homebrew/opt/openssl@1.1 ..`
1. Build:
`make`

View file

@ -118,6 +118,7 @@ nmake
```
To build a debug build, you'll also need to add the `-s compiler.runtime_type=Debug` flag to the `conan install` invocation. See [this StackOverflow post](https://stackoverflow.com/questions/59828611/windeployqt-doesnt-deploy-qwindowsd-dll-for-a-debug-application/75607313#75607313)
To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake` command.
#### Deploying Qt libraries

View file

@ -50,4 +50,5 @@ This will require more than 30GB of free space on your hard drive.
cmake --build . --parallel <threads> --config Release
```
When using CMD, use `-DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake` to specify the toolchain.
To build with plugins add `-DCHATTERINO_PLUGINS=ON` to `cmake -B build` command.
1. Run `.\bin\chatterino2.exe`

View file

@ -2,113 +2,156 @@
## Unversioned
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060)
- Major: Add an Image Uploader tab to the Settings. (#4995)
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847)
- Minor: Allow running `/ban`, `/timeout`, `/unban`, and `/untimeout` on User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957)
- Minor: The `/usercard` command now accepts user ids. (#4934)
- Minor: Add menu actions to reply directly to a message or the original thread root. (#4923)
- Minor: The `/reply` command now replies to the latest message of the user. (#4919)
- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978)
- Minor: Add an option to use new experimental smarter emote completion. (#4987)
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - right now only supporting bot badges for your chat bots. (#5119)
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047)
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
- Major: Release plugins alpha. (#5288)
- Major: Improve high-DPI support on Windows. (#4868, #5391)
- Minor: Add option to customise Moderation buttons with images. (#5369)
- Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
- Dev: Use Qt's high DPI scaling. (#4868)
- Dev: Add doxygen build target. (#5377)
- Dev: Make printing of strings in tests easier. (#5379)
- Dev: Refactor and document `Scrollbar`. (#5334, #5393)
## 2.5.1
- Bugfix: Fixed links without a protocol not being clickable. (#5345)
## 2.5.0
- Major: Twitch follower emotes can now be correctly tabbed in other channels when you are subscribed to the channel the emote is from. (#4922)
- Major: Added `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
- Major: Moderators can now see restricted chat messages and suspicious treatment updates. (#5056, #5060)
- Minor: Migrated to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: Moderation commands such as `/ban`, `/timeout`, `/unban`, and `/untimeout` can now be used via User IDs by using the `id:123` syntax (e.g. `/timeout id:22484632 1m stop winning`). (#4945, #4956, #4957)
- Minor: The `/usercard` command now accepts user ids. (`/usercard id:22484632`) (#4934)
- Minor: Added menu actions to reply directly to a message or the original thread root. (#4923)
- Minor: The `/reply` command now replies to the latest message from the user. Due to this change, the message you intended to reply to is now shown in the reply context, instead of the first message in a thread. (#4919)
- Minor: The chatter list button is now hidden if you don't have moderator privileges. (#5245)
- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237)
- Minor: Allowed theming of tab live and rerun indicators. (#5188)
- Minor: The _Restart on crash_ setting works again on Windows. (#5012)
- Minor: Added an option to use new experimental smarter emote completion. (#4987)
- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - currently only supports bot badges for your chat bots. (#5119)
- Minor: Added support to send /announce[color] commands. Colored announcements only appear with the chosen color in Twitch chat. (#5250)
- Minor: The whisper highlight color can now be configured through the settings. (#5053)
- Minor: Added an option to always include the broadcaster in user completions. This is enabled by default. (#5193, #5244)
- Minor: Added missing periods at various moderator messages and commands. (#5061)
- Minor: Improved color selection and display. (#5057)
- Minor: Improved Streamlink documentation in the settings dialog. (#5076)
- Minor: Normalized the input padding between light & dark themes. (#5095)
- Minor: Add `--activate <channel>` (or `-a`) command line option to activate or add a Twitch channel. (#5111)
- Minor: Chatters from recent-messages are now added to autocompletion. (#5116)
- Minor: Added a _System_ theme that updates according to the system's color scheme (requires Qt 6.5). (#5118)
- Minor: Added icons for newer versions of macOS. (#5148)
- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197)
- Minor: Added a warning message if you have multiple commands with the same trigger. (#4322)
- Minor: Chatters from message history are now added to autocompletion. (#5116)
- Minor: Added support for the `{input.text}` placeholder in the **Split** -> **Run a command** hotkey. (#5130)
- Minor: Add a new Channel API for experimental plugins feature. (#5141, #5184, #5187)
- Minor: Added `--activate <channel>` (or `-a`) command line option to focus or add a certain Twitch channel on startup. (#5111)
- Minor: Added the `--incognito/--no-incognito` options to the `/openurl` command, allowing you to override the "Open links in incognito/private mode" setting. (#5149, #5197)
- Minor: Added the ability to change the top-most status of a window regardless of the _Always on top_ setting (right click the notebook). (#5135)
- Minor: Introduce `c2.later()` function to Lua API. (#5154)
- Minor: Live streams that are marked as reruns now mark a tab as yellow instead of red. (#5176, #5237)
- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182)
- Minor: Added the ability to show AutoMod caught messages in mentions. (#5215)
- Minor: Added the ability to configure the color of highlighted AutoMod caught messages. (#5215)
- Minor: Allow theming of tab live and rerun indicators. (#5188)
- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182)
- Minor: Added icons for newer versions of macOS. (#5148)
- Minor: Added more menu items in macOS menu bar. (#5266)
- Minor: Improved color selection and display. (#5057)
- Minor: Added a _System_ theme setting that updates according to the system's color scheme (requires Qt 6.5). (#5118)
- Minor: Normalized the input padding between light & dark themes. (#5095)
- Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Added a fallback theme field to custom themes that will be used in case the custom theme does not contain a color Chatterino needs. If no fallback theme is specified, we'll pull the color from the included Dark or Light theme. (#5198)
- Minor: Added a new completion API for experimental plugins feature. (#5000, #5047)
- Minor: Added a new Channel API for experimental plugins feature. (#5141, #5184, #5187)
- Minor: Introduce `c2.later()` function to Lua API. (#5154)
- Minor: Added `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
- Minor: Added wrappers for Lua `io` library for experimental plugins feature. (#5231)
- Minor: Added permissions to experimental plugins feature. (#5231)
- Minor: Added missing periods at various moderator messages and commands. (#5061)
- Minor: Improved Streamlink documentation in the settings dialog. (#5076)
- Minor: The installer now checks for the VC Runtime version and shows more info when it's outdated. (#4847)
- Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978)
- Minor: Added an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
- Minor: Image links now reflect the scale of their image instead of an internal label. (#5201)
- Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226)
- Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209)
- Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231)
- Minor: Add permissions to experimental plugins feature. (#5231)
- Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275)
- Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280)
- Minor: Changed the layout of the about page. (#5287)
- Minor: Add duration to multi-month anon sub gift messages. (#5293)
- Minor: Added context menu action to toggle visibility of offline tabs. (#5318)
- Minor: Report sub duration for more multi-month gift cases. (#5319)
- Minor: Improved error reporting for the automatic streamer mode detection on Linux and macOS. (#5321)
- Bugfix: Fixed a crash that could occur on Wayland when using the image uploader. (#5314)
- Bugfix: Fixed split tooltip getting stuck in some cases. (#5309)
- Bugfix: Fixed the version string not showing up as expected in Finder on macOS. (#5311)
- Bugfix: Fixed links having `http://` added to the beginning in certain cases. (#5323)
- Bugfix: Fixed topmost windows from losing their status after opening dialogs on Windows. (#5330)
- Bugfix: Fixed a gap appearing when using filters on `/watching`. (#5329)
- Bugfix: Removed the remnant "Show chatter list" menu entry for non-moderators. (#5336)
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
- Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800)
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
- Bugfix: Fixed a performance issue when displaying replies to certain messages. (#4807)
- Bugfix: Fixed an issue where certain parts of the split input wouldn't focus the split when clicked. (#4958)
- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771)
- Bugfix: Fixed `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800)
- Bugfix: Fixed Usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511)
- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172)\
- Bugfix: User text input within watch streak notices now correctly shows up. (#5029)
- Bugfix: Fixed selection of tabs after closing a tab when using "Live Tabs Only". (#4770)
- Bugfix: Fixed input in reply thread popup losing focus when dragging. (#4815)
- Bugfix: Fixed the Quick Switcher (CTRL+K) from sometimes showing up on the wrong window. (#4819)
- Bugfix: Fixed input in the reply thread popup losing focus when dragging said window. (#4815)
- Bugfix: Fixed the Quick Switcher (CTRL+K) sometimes showing up on the wrong window. (#4819)
- Bugfix: Fixed the font switcher not remembering what font you had previously selected. (#5224)
- Bugfix: Fixed too much text being copied when copying chat messages. (#4812, #4830, #4839)
- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855)
- Bugfix: Fixed empty page being added when showing out of bounds dialog. (#4849)
- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained text input. (#5117)
- Bugfix: Fixed issue on Windows preventing the title bar from being dragged in the top left corner. (#4873)
- Bugfix: Fixed an issue where Streamer Mode did not detect that OBS was running on MacOS. (#5260)
- Bugfix: Remove ":" from the message the user is replying to if it's a /me message. (#5263)
- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229)
- Bugfix: Fixed an issue where the setting `Only search for emote autocompletion at the start of emote names` wouldn't disable if it was enabled when the client started. (#4855)
- Bugfix: Fixed an empty page being added when showing the out of bounds dialog. (#4849)
- Bugfix: Fixed an issue preventing searching a redemption by it's title when the redemption contained user text input. (#5117)
- Bugfix: Fixed an issue where reply context didn't render correctly if an emoji was touching text. (#4875, #4977, #5174)
- Bugfix: Fixed the input completion popup from disappearing when clicking on it on Windows and macOS. (#4876)
- Bugfix: Fixed the input completion popup sometimes disappearing when clicking on it on Windows and macOS. (#4876)
- Bugfix: Fixed Twitch badges not loading correctly in the badge highlighting setting page. (#5223)
- Bugfix: Fixed double-click text selection moving its position with each new message. (#4898)
- Bugfix: Fixed an issue where notifications on Windows would contain no or an old avatar. (#4899)
- Bugfix: Fixed headers of tables in the settings switching to bold text when selected. (#4913)
- Bugfix: Fixed an issue in the `/live` split that caused some channels to not get grayed-out when they went offline. (#5172)
- Bugfix: Fixed tooltips appearing too large and/or away from the cursor. (#4920)
- Bugfix: Fixed a crash when clicking `More messages below` button in a usercard and closing it quickly. (#4933)
- Bugfix: Fixed thread popup window missing messages for nested threads. (#4923)
- Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949)
- Bugfix: Fixed triple click on message also selecting moderation buttons. (#4961)
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126)
- Bugfix: Fixed triple-click on message also selecting moderation buttons. (#4961)
- Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110)
- Bugfix: Fixed emotes being reloaded when pressing "Cancel" in the settings dialog, causing a slowdown. (#5240)
- Bugfix: Fixed double-click selection not correctly selecting words that were split onto multiple lines. (#5243)
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126)
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126)
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126)
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
- Bugfix: Fixed an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011)
- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972)
- Bugfix: Fixed a rare crash with the Image Uploader when closing a split right after starting an upload. (#4971)
- Bugfix: Fixed an issue on macOS where the Image Uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011)
- Bugfix: The usercard button is now hidden in the User Info Popup when in special channels. (#4972)
- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994, #5175)
- Bugfix: Fixed some windows appearing between screens. (#4797)
- Bugfix: Fixed a crash that could occur when clicking `More messages below` button in a usercard and closing it quickly. (#4933)
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051)
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051)
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040)
- Bugfix: Fixes to section deletion in text input fields. (#5013)
- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246)
- Bugfix: Show user text input within watch streak notices. (#5029)
- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052)
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
- Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077)
- Bugfix: Fixed an issue where you had to click the `reply` button twice if you already had that users @ in your input box. (#5173)
- Bugfix: Fixed popup windows not persisting between restarts. (#5081)
- Bugfix: Fixed splits not retaining their focus after minimizing. (#5080)
- Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106)
- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279, #5291)
- Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269)
- Bugfix: Reply contexts now use the color of the replied-to message. (#5145)
- Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166)
- Bugfix: Fixed link info not updating without moving the cursor. (#5178)
- Bugfix: Fixed an upload sometimes failing when copying an image from a browser if it contained extra properties. (#5156)
- Bugfix: Fixed tooltips getting out of bounds when loading images. (#5186)
- Bugfix: Fixed the "Cancel" button in the settings dialog only working after opening the settings dialog twice. (#5229)
- Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230)
- Bugfix: Fixed split header tooltips appearing too tall. (#5232)
- Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248)
- Bugfix: Fixed pause indicator not disappearing in some cases. (#5265)
- Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511)
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
- Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246)
- Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771)
- Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282)
- Bugfix: Fixed highlights triggering for ignored users in announcements. (#5295)
- Dev: Changed the order of the query parameters for Twitch player URLs. (#5326)
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
- Dev: Change clang-format from v14 to v16. (#4929)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
@ -163,11 +206,12 @@
- Dev: Load less message history upon reconnects. (#5001, #5018)
- Dev: Removed the `NullablePtr` class. (#5091)
- Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014, #5108)
- Dev: Compile Lua as a C library. (#5251)
- Dev: Fixed most compiler warnings. (#5028, #5137)
- Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747)
- Dev: Refactor Args to be less of a singleton. (#5041)
- Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045)
- Dev: Autogenerate docs/plugin-meta.lua. (#5055)
- Dev: Autogenerate docs/plugin-meta.lua. (#5055, #5283)
- Dev: Changed Ubuntu & AppImage builders to statically link Qt. (#5151)
- Dev: Refactor `NetworkPrivate`. (#5063)
- Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102)
@ -178,12 +222,16 @@
- Dev: Added signal to invalidate paint buffers of channel views without forcing a relayout. (#5123)
- Dev: Specialize `Atomic<std::shared_ptr<T>>` if underlying standard library supports it. (#5133)
- Dev: Added the `developer_name` field to the Linux AppData specification. (#5138)
- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200)
- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200, #5276)
- Dev: Added estimation for image sizes to avoid layout shifts. (#5192)
- Dev: Added the `launachable` entry to Linux AppData. (#5210)
- Dev: Cleaned up and optimized resources. (#5222)
- Dev: Refactor `StreamerMode`. (#5216, #5236)
- Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225)
- Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258)
- Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254, #5297)
- Dev: `clang-tidy` CI now uses Qt 6. (#5273)
- Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278)
## 2.4.6

View file

@ -28,7 +28,7 @@ option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF)
option(BUILD_TRANSLATIONS "" OFF)
option(BUILD_SHARED_LIBS "" OFF)
option(CHATTERINO_LTO "Enable LTO for all targets" OFF)
option(CHATTERINO_PLUGINS "Enable EXPERIMENTAL plugin support in Chatterino" OFF)
option(CHATTERINO_PLUGINS "Enable ALPHA plugin support in Chatterino" OFF)
option(CHATTERINO_UPDATER "Enable update checks" ON)
mark_as_advanced(CHATTERINO_UPDATER)
@ -41,7 +41,7 @@ if(BUILD_BENCHMARKS)
endif()
project(chatterino
VERSION 2.4.6
VERSION 2.5.1
DESCRIPTION "Chat client for twitch.tv"
HOMEPAGE_URL "https://chatterino.com/"
)
@ -197,6 +197,7 @@ find_package(PajladaSerialize REQUIRED)
find_package(PajladaSignals REQUIRED)
find_package(LRUCache REQUIRED)
find_package(MagicEnum REQUIRED)
find_package(Doxygen)
if (USE_SYSTEM_PAJLADA_SETTINGS)
find_package(PajladaSettings REQUIRED)

View file

@ -6,8 +6,6 @@
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>CFBundleIdentifier</key>

14
docs/chatterino.d.ts vendored
View file

@ -75,6 +75,13 @@ declare module c2 {
handler: (ctx: CommandContext) => void
): boolean;
class CompletionEvent {
query: string;
full_text_content: string;
cursor_position: number;
is_first_word: boolean;
}
class CompletionList {
values: String[];
hide_others: boolean;
@ -84,12 +91,7 @@ declare module c2 {
CompletionRequested = "CompletionRequested",
}
type CbFuncCompletionsRequested = (
query: string,
full_text_content: string,
cursor_position: number,
is_first_word: boolean
) => CompletionList;
type CbFuncCompletionsRequested = (ev: CompletionEvent) => CompletionList;
type CbFunc<T> = T extends EventType.CompletionRequested
? CbFuncCompletionsRequested
: never;

View file

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

View file

@ -6,22 +6,14 @@
c2 = {}
---@class IWeakResource
--- Returns true if the channel this object points to is valid.
--- If the object expired, returns false
--- If given a non-Channel object, it errors.
---@return boolean
function IWeakResource:is_valid() end
---@alias LogLevel integer
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
---@alias c2.LogLevel integer
---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel }
c2.LogLevel = {}
---@alias EventType integer
---@type { CompletionRequested: EventType }
---@alias c2.EventType integer
---@type { CompletionRequested: c2.EventType }
c2.EventType = {}
---@class CommandContext
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
---@field channel Channel The channel the command was executed in.
@ -29,20 +21,31 @@ c2.EventType = {}
---@class CompletionList
---@field values string[] The completions
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
-- Now including data from src/common/Channel.hpp.
---@class CompletionEvent
---@field query string The word being completed
---@field full_text_content string Content of the text input
---@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes)
---@field is_first_word boolean True if this is the first word in the input
-- Begin src/common/Channel.hpp
---@alias ChannelType integer
---@type { None: ChannelType }
---@type { None: ChannelType, Direct: ChannelType, Twitch: ChannelType, TwitchWhispers: ChannelType, TwitchWatching: ChannelType, TwitchMentions: ChannelType, TwitchLive: ChannelType, TwitchAutomod: ChannelType, TwitchEnd: ChannelType, Irc: ChannelType, Misc: ChannelType }
ChannelType = {}
-- Back to src/controllers/plugins/LuaAPI.hpp.
-- Now including data from src/controllers/plugins/api/ChannelRef.hpp.
--- This enum describes a platform for the purpose of searching for a channel.
--- Currently only Twitch is supported because identifying IRC channels is tricky.
-- End src/common/Channel.hpp
-- Begin src/controllers/plugins/api/ChannelRef.hpp
---@alias Platform integer
--- This enum describes a platform for the purpose of searching for a channel.
--- Currently only Twitch is supported because identifying IRC channels is tricky.
---@type { Twitch: Platform }
Platform = {}
---@class Channel: IWeakResource
---@class Channel
Channel = {}
--- Returns true if the channel this object points to is valid.
--- If the object expired, returns false
@ -82,11 +85,9 @@ function Channel:add_system_message(message) end
--- Compares the channel Type. Note that enum values aren't guaranteed, just
--- that they are equal to the exposed enum.
---
---@return bool
---@return boolean
function Channel:is_twitch_channel() end
--- Twitch Channel specific functions
--- Returns a copy of the channel mode settings (subscriber only, r9k etc.)
---
---@return RoomModes
@ -119,15 +120,10 @@ function Channel:is_mod() end
---@return boolean
function Channel:is_vip() end
--- Misc
---@return string
function Channel:__tostring() end
--- Static functions
--- Finds a channel by name.
---
--- Misc channels are marked as Twitch:
--- - /whispers
--- - /mentions
@ -142,19 +138,15 @@ function Channel.by_name(name, platform) end
--- Finds a channel by the Twitch user ID of its owner.
---
---@param string id ID of the owner of the channel.
---@param id string ID of the owner of the channel.
---@return Channel?
function Channel.by_twitch_id(string) end
function Channel.by_twitch_id(id) end
---@class RoomModes
---@field unique_chat boolean You might know this as r9kbeta or robot9000.
---@field subscriber_only boolean
---@field emotes_only boolean Whether or not text is allowed in messages.
--- Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
---@field unique_chat number? Time in minutes you need to follow to chat or nil.
---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
---@field follower_only number? Time in minutes you need to follow to chat or nil.
---@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
---@class StreamStatus
@ -164,7 +156,8 @@ function Channel.by_twitch_id(string) end
---@field title string Stream title or last stream title
---@field game_name string
---@field game_id string
-- Back to src/controllers/plugins/LuaAPI.hpp.
-- End src/controllers/plugins/api/ChannelRef.hpp
--- Registers a new command called `name` which when executed will call `handler`.
---
@ -176,12 +169,12 @@ function c2.register_command(name, handler) end
--- Registers a callback to be invoked when completions for a term are requested.
---
---@param type "CompletionRequested"
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
function c2.register_callback(type, func) end
--- Writes a message to the Chatterino log.
---
---@param level LogLevel The desired level.
---@param level c2.LogLevel The desired level.
---@param ... any Values to log. Should be convertible to a string with `tostring()`.
function c2.log(level, ...) end

View file

@ -167,7 +167,7 @@ Limitations/known issues:
#### `register_callback("CompletionRequested", handler)`
Registers a callback (`handler`) to process completions. The callback gets the following parameters:
Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries:
- `query`: The queried word.
- `full_text_content`: The whole input.
@ -190,8 +190,8 @@ end
c2.register_callback(
"CompletionRequested",
function(query, full_text_content, cursor_position, is_first_word)
if ("!join"):startswith(query) then
function(event)
if ("!join"):startswith(event.query) then
---@type CompletionList
return { hide_others = true, values = { "!join" } }
end

View file

@ -50,4 +50,4 @@ target_include_directories(lua
PUBLIC
${LUA_INCLUDE_DIRS}
)
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE CXX)
set_source_files_properties(${LUA_SRC} PROPERTIES LANGUAGE C)

@ -1 +1 @@
Subproject commit e288c5a91883793d14ed9e9d93464f6ee0b08915
Subproject commit 0897c0a4289ef3a8d45761266124613f364bef60

@ -1 +1 @@
Subproject commit ceac9c7e97d2d2b97f40ecd0b421e358d7525cbc
Subproject commit 03e8af1934e6151edfe8a44dfb025b747a31acdf

BIN
resources/avatars/anon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -34,6 +34,15 @@
<binary>chatterino</binary>
</provides>
<releases>
<release version="2.5.1" date="2024-04-28">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.5.1</url>
</release>
<release version="2.5.0" date="2024-04-21">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0</url>
</release>
<release version="2.5.0~beta1" date="2024-04-07">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.5.0-beta.1</url>
</release>
<release version="2.4.6" date="2023-09-30">
<url>https://github.com/Chatterino/chatterino2/releases/tag/v2.4.6</url>
</release>

View file

@ -3,80 +3,90 @@
# TODO: Parse this into a CONTRIBUTORS.md too
# Adding yourself? Copy and paste this template at the bottom of this file and fill in the fields in a PR!
# Name | Link | Avatar (Loaded as a resource, avatars are not required) | Title (description of work done).
# Name | Link | Avatar (Loaded as a resource, avatars are not required).
# Avatar should be located in avatars/ directory. Its size should be 128x128 (get it from https://github.com/username.png?size=128).
# Make sure to reduce avatar's size as much as possible with tool like pngcrush or optipng (if the file is in png format).
# Contributor is what we use for someone who has contributed in general (like sent a programming-related PR).
fourtf | https://fourtf.com | :/avatars/fourtf.png | Author, main developer
pajlada | https://pajlada.se | :/avatars/pajlada.png | Collaborator, co-developer
zneix | https://github.com/zneix | :/avatars/zneix.png | Collaborator
Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png | Collaborator
YungLPR | https://github.com/leon-richardt | | Collaborator
dnsge | https://github.com/dnsge | | Collaborator
Felanbird | https://github.com/Felanbird | | Collaborator
kornes | https://github.com/kornes | | Collaborator
@header Maintainers
Cranken | https://github.com/Cranken | | Contributor
hemirt | https://github.com/hemirt | | Contributor
LajamerrMittesdine | https://github.com/LajamerrMittesdine | | Contributor
coral | https://github.com/coral | | Contributor, design
apa420 | https://github.com/apa420 | | Contributor
DatGuy1 | https://github.com/DatGuy1 | | Contributor
Confuseh | https://github.com/Confuseh | | Contributor
ch-ems | https://github.com/ch-ems | | Contributor
Bur0k | https://github.com/Bur0k | | Contributor
nuuls | https://github.com/nuuls | | Contributor
Chronophylos | https://github.com/Chronophylos | | Contributor
Ckath | https://github.com/Ckath | | Contributor
matijakevic | https://github.com/matijakevic | | Contributor
nforro | https://github.com/nforro | | Contributor
vanolpfan | https://github.com/vanolpfan | | Contributor
23rd | https://github.com/23rd | | Contributor
machgo | https://github.com/machgo | | Contributor
TranRed | https://github.com/TranRed | | Contributor
RAnders00 | https://github.com/RAnders00 | | Contributor
gempir | https://github.com/gempir | | Contributor
mfmarlow | https://github.com/mfmarlow | | Contributor
y0dax | https://github.com/y0dax | | Contributor
Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg | Contributor
matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg | Contributor
Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png | Contributor
Talen | https://github.com/talneoran | | Contributor
SLCH | https://github.com/SLCH | :/avatars/slch.png | Contributor
ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png | Contributor
xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png | Contributor
1xelerate | https://github.com/xel86 | :/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
hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png | Contributor
iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png | Contributor
Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png | Contributor
Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png | Contributor
mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png | Contributor
Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png | Contributor
03y | https://github.com/03y | | Contributor
ScrubN | https://github.com/ScrubN | | Contributor
Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png | Contributor
2547techno | https://github.com/2547techno | :/avatars/techno.png | Contributor
ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png | Contributor
olafyang | https://github.com/olafyang | | Contributor
chrrs | https://github.com/chrrs | | Contributor
4rneee | https://github.com/4rneee | | Contributor
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor
SputNikPlop | https://github.com/SputNikPlop | | Contributor
fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor
KleberPF | https://github.com/KleberPF | | Contributor
fourtf | https://fourtf.com | :/avatars/fourtf.png
pajlada | https://pajlada.se | :/avatars/pajlada.png
@header Collaborators
zneix | https://github.com/zneix | :/avatars/zneix.png
Mm2PL | https://github.com/mm2pl | :/avatars/mm2pl.png
YungLPR | https://github.com/leon-richardt |
dnsge | https://github.com/dnsge |
Felanbird | https://github.com/Felanbird |
kornes | https://github.com/kornes |
@header Contributors
Cranken | https://github.com/Cranken |
hemirt | https://github.com/hemirt |
LajamerrMittesdine | https://github.com/LajamerrMittesdine |
coral | https://github.com/coral |
apa420 | https://github.com/apa420 |
DatGuy1 | https://github.com/DatGuy1 |
Confuseh | https://github.com/Confuseh |
ch-ems | https://github.com/ch-ems |
Bur0k | https://github.com/Bur0k |
nuuls | https://github.com/nuuls |
Chronophylos | https://github.com/Chronophylos |
Ckath | https://github.com/Ckath |
matijakevic | https://github.com/matijakevic |
nforro | https://github.com/nforro |
vanolpfan | https://github.com/vanolpfan |
23rd | https://github.com/23rd |
machgo | https://github.com/machgo |
TranRed | https://github.com/TranRed |
RAnders00 | https://github.com/RAnders00 |
gempir | https://github.com/gempir |
mfmarlow | https://github.com/mfmarlow |
y0dax | https://github.com/y0dax |
Iulian Onofrei | https://github.com/revolter | :/avatars/revolter.jpg
matthewde | https://github.com/m4tthewde | :/avatars/matthewde.jpg
Karar Al-Remahy | https://github.com/KararTY | :/avatars/kararty.png
Talen | https://github.com/talneoran |
SLCH | https://github.com/SLCH | :/avatars/slch.png
ALazyMeme | https://github.com/alazymeme | :/avatars/alazymeme.png
xHeaveny_ | https://github.com/xHeaveny | :/avatars/xheaveny.png
1xelerate | https://github.com/xel86 | :/avatars/_1xelerate.png
acdvs | https://github.com/acdvs |
karl-police | https://github.com/karl-police | :/avatars/karlpolice.png
brian6932 | https://github.com/brian6932 | :/avatars/brian6932.png
hicupalot | https://github.com/hicupalot | :/avatars/hicupalot.png
iProdigy | https://github.com/iProdigy | :/avatars/iprodigy.png
Jaxkey | https://github.com/Jaxkey | :/avatars/jaxkey.png
Explooosion | https://github.com/Explooosion-code | :/avatars/explooosion_code.png
mohad12211 | https://github.com/mohad12211 | :/avatars/mohad12211.png
Wissididom | https://github.com/Wissididom | :/avatars/wissididom.png
03y | https://github.com/03y |
ScrubN | https://github.com/ScrubN |
Cyclone | https://github.com/PsycloneTM | :/avatars/cyclone.png
2547techno | https://github.com/2547techno | :/avatars/techno.png
ZonianMidian | https://github.com/ZonianMidian | :/avatars/zonianmidian.png
olafyang | https://github.com/olafyang |
chrrs | https://github.com/chrrs |
4rneee | https://github.com/4rneee |
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png
SputNikPlop | https://github.com/SputNikPlop |
fraxx | https://github.com/fraxxio | :/avatars/fraxx.png
KleberPF | https://github.com/KleberPF |
nealxm | https://github.com/nealxm | :/avatars/nealxm.png
# If you are a contributor add yourself above this line
Defman21 | https://github.com/Defman21 | | Documentation
vilgotf | https://github.com/vilgotf | | Documentation
Ian321 | https://github.com/Ian321 | | Documentation
Yardanico | https://github.com/Yardanico | | Documentation
huti26 | https://github.com/huti26 | | Documentation
chrisduerr | https://github.com/chrisduerr | | Documentation
@header Documentation
Defman21 | https://github.com/Defman21 |
vilgotf | https://github.com/vilgotf |
Ian321 | https://github.com/Ian321 |
Yardanico | https://github.com/Yardanico |
huti26 | https://github.com/huti26 |
chrisduerr | https://github.com/chrisduerr |
# Otherwise add yourself right above this one

View file

@ -1,11 +1,11 @@
* {
font-size: <font-size>px;
font-size: 14px;
font-family: "Segoe UI";
}
QCheckBox::indicator {
width: <checkbox-size>px;
height: <checkbox-size>px;
width: 14px;
height: 14px;
}
chatterino--ComboBox {

View file

@ -12,25 +12,26 @@ It assumes comments look like:
- Do not have any useful info on '/**' and '*/' lines.
- Class members are not allowed to have non-@command lines and commands different from @lua@field
When this scripts sees "@brief", any further lines of the comment will be ignored
Only entire comment blocks are used. One comment block can describe at most one
entity (function/class/enum). Blocks without commands are ignored.
Valid commands are:
1. @exposeenum [dotted.name.in_lua.last_part]
Define a table with keys of the enum. Values behind those keys aren't
written on purpose.
This generates three lines:
- An type alias of [last_part] to integer,
- A type description that describes available values of the enum,
- A global table definition for the num
2. @lua[@command]
2. @exposed [c2.name]
Generates a function definition line from the last `@lua@param`s.
3. @lua[@command]
Writes [@command] to the file as a comment, usually this is @class, @param, @return, ...
@lua@class and @lua@field have special treatment when it comes to generation of spacing new lines
3. @exposed [c2.name]
Generates a function definition line from the last `@lua@param`s.
Non-command lines of comments are written with a space after '---'
"""
from io import TextIOWrapper
from pathlib import Path
import re
from typing import Optional
BOILERPLATE = """
---@meta Chatterino2
@ -41,14 +42,6 @@ BOILERPLATE = """
c2 = {}
---@class IWeakResource
--- Returns true if the channel this object points to is valid.
--- If the object expired, returns false
--- If given a non-Channel object, it errors.
---@return boolean
function IWeakResource:is_valid() end
"""
repo_root = Path(__file__).parent.parent
@ -58,116 +51,274 @@ lua_meta = repo_root / "docs" / "plugin-meta.lua"
print("Writing to", lua_meta.relative_to(repo_root))
def process_file(target, out):
print("Reading from", target.relative_to(repo_root))
with target.open("r") as f:
def strip_line(line: str):
return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip()
def is_comment_start(line: str):
return line.startswith("/**")
def is_enum_class(line: str):
return line.startswith("enum class")
def is_class(line: str):
return line.startswith(("class", "struct"))
class Reader:
lines: list[str]
line_idx: int
def __init__(self, lines: list[str]) -> None:
self.lines = lines
self.line_idx = 0
def line_no(self) -> int:
"""Returns the current line number (starting from 1)"""
return self.line_idx + 1
def has_next(self) -> bool:
"""Returns true if there are lines left to read"""
return self.line_idx < len(self.lines)
def peek_line(self) -> Optional[str]:
"""Reads the line the cursor is at"""
if self.has_next():
return self.lines[self.line_idx].strip()
return None
def next_line(self) -> Optional[str]:
"""Consumes and returns one line"""
if self.has_next():
self.line_idx += 1
return self.lines[self.line_idx - 1].strip()
return None
def next_doc_comment(self) -> Optional[list[str]]:
"""Reads a documentation comment (/** ... */) and advances the cursor"""
lines = []
# find the start
while (line := self.next_line()) is not None and not is_comment_start(line):
pass
if line is None:
return None
stripped = strip_line(line)
if stripped:
lines.append(stripped)
if stripped.endswith("*/"):
return lines if lines else None
while (line := self.next_line()) is not None:
if line.startswith("*/"):
break
stripped = strip_line(line)
if not stripped:
continue
if stripped.startswith("@"):
lines.append(stripped)
continue
if not lines:
lines.append(stripped)
else:
lines[-1] += "\n--- " + stripped
return lines if lines else None
def read_class_body(self) -> list[list[str]]:
"""The reader must be at the first line of the class/struct body. All comments inside the class are returned."""
items = []
while (line := self.peek_line()) is not None:
if line.startswith("};"):
self.next_line()
break
if not is_comment_start(line):
self.next_line()
continue
doc = self.next_doc_comment()
if not doc:
break
items.append(doc)
return items
def read_enum_variants(self) -> list[str]:
"""The reader must be before an enum class definition (possibly with some comments before). It returns all variants."""
items = []
is_comment = False
while (line := self.peek_line()) is not None and not line.startswith("};"):
self.next_line()
if is_comment:
if line.endswith("*/"):
is_comment = False
continue
if line.startswith("/*"):
is_comment = True
continue
if line.startswith("//"):
continue
if line.endswith("};"): # oneline declaration
opener = line.find("{") + 1
closer = line.find("}")
items = [
line.split("=", 1)[0].strip()
for line in line[opener:closer].split(",")
]
break
if line.startswith("enum class"):
continue
items.append(line.rstrip(","))
return items
def finish_class(out, name):
out.write(f"{name} = {{}}\n")
def printmsg(path: Path, line: int, message: str):
print(f"{path.relative_to(repo_root)}:{line} {message}")
def panic(path: Path, line: int, message: str):
printmsg(path, line, message)
exit(1)
def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper):
if not comments[0].startswith("@"):
out.write(f"--- {comments[0]}\n---\n")
comments = comments[1:]
params = []
for comment in comments[:-1]:
if not comment.startswith("@lua"):
panic(path, line, f"Invalid function specification - got '{comment}'")
if comment.startswith("@lua@param"):
params.append(comment.split(" ", 2)[1])
out.write(f"---{comment.removeprefix('@lua')}\n")
if not comments[-1].startswith("@exposed "):
panic(path, line, f"Invalid function exposure - got '{comments[-1]}'")
name = comments[-1].split(" ", 1)[1]
printmsg(path, line, f"function {name}")
lua_params = ", ".join(params)
out.write(f"function {name}({lua_params}) end\n\n")
def read_file(path: Path, out: TextIOWrapper):
print("Reading", path.relative_to(repo_root))
with path.open("r") as f:
lines = f.read().splitlines()
# Are we in a doc comment?
comment: bool = False
# This is set when @brief is encountered, making the rest of the comment be
# ignored
ignore_this_comment: bool = False
# Last `@lua@param`s seen - for @exposed generation
last_params_names: list[str] = []
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
is_class = False
# The name of the next enum in lua world
expose_next_enum_as: str | None = None
# Name of the current enum in c++ world, used to generate internal typenames for
current_enum_name: str | None = None
for line_num, line in enumerate(lines):
line = line.strip()
loc = f'{target.relative_to(repo_root)}:{line_num}'
if line.startswith("enum class "):
line = line.removeprefix("enum class ")
temp = line.split(" ", 2)
current_enum_name = temp[0]
if not expose_next_enum_as:
print(
f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
)
current_enum_name = None
reader = Reader(lines)
while reader.has_next():
doc_comment = reader.next_doc_comment()
if not doc_comment:
break
header_comment = None
if not doc_comment[0].startswith("@"):
if len(doc_comment) == 1:
continue
current_enum_name = expose_next_enum_as.split(".", 1)[-1]
out.write("---@alias " + current_enum_name + " integer\n")
out.write("---@type { ")
# temp[1] is '{'
if len(temp) == 2: # no values on this line
continue
line = temp[2]
if current_enum_name is not None:
for i, tok in enumerate(line.split(" ")):
if tok == "};":
break
entry = tok.removesuffix(",")
if i != 0:
out.write(", ")
out.write(entry + ": " + current_enum_name)
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}")
current_enum_name = None
expose_next_enum_as = None
continue
if line.startswith("/**"):
comment = True
continue
elif "*/" in line:
comment = False
ignore_this_comment = False
if not is_class:
out.write("\n")
continue
if not comment:
continue
if ignore_this_comment:
continue
line = line.replace("*", "", 1).lstrip()
if line == "":
out.write("---\n")
elif line.startswith('@brief '):
# Doxygen comment, on a C++ only method
ignore_this_comment = True
elif line.startswith("@exposeenum "):
expose_next_enum_as = line.split(" ", 1)[1]
elif line.startswith("@exposed "):
exp = line.replace("@exposed ", "", 1)
params = ", ".join(last_params_names)
out.write(f"function {exp}({params}) end\n")
print(f"{loc} Wrote function {exp}(...)")
last_params_names = []
elif line.startswith("@includefile "):
filename = line.replace("@includefile ", "", 1)
output.write(f"-- Now including data from src/{filename}.\n")
process_file(repo_root / 'src' / filename, output)
output.write(f'-- Back to {target.relative_to(repo_root)}.\n')
elif line.startswith("@lua"):
command = line.replace("@lua", "", 1)
if command.startswith("@param"):
last_params_names.append(command.split(" ", 2)[1])
elif command.startswith("@class"):
print(f"{loc} Writing {command}")
if is_class:
out.write("\n")
is_class = True
elif not command.startswith("@field"):
is_class = False
out.write("---" + command + "\n")
header_comment = doc_comment[0]
header = doc_comment[1:]
else:
if is_class:
is_class = False
header = doc_comment
# include block
if header[0].startswith("@includefile "):
for comment in header:
if not comment.startswith("@includefile "):
panic(
path,
reader.line_no(),
f"Invalid include block - got line '{comment}'",
)
filename = comment.split(" ", 1)[1]
out.write(f"-- Begin src/{filename}\n\n")
read_file(repo_root / "src" / filename, out)
out.write(f"-- End src/{filename}\n\n")
continue
# enum
if header[0].startswith("@exposeenum "):
if len(header) > 1:
panic(
path,
reader.line_no(),
f"Invalid enum exposure - one command expected, got {len(header)}",
)
name = header[0].split(" ", 1)[1]
printmsg(path, reader.line_no(), f"enum {name}")
out.write(f"---@alias {name} integer\n")
if header_comment:
out.write(f"--- {header_comment}\n")
out.write("---@type { ")
out.write(
", ".join(
[f"{variant}: {name}" for variant in reader.read_enum_variants()]
)
)
out.write(" }\n")
out.write(f"{name} = {{}}\n\n")
continue
# class
if header[0].startswith("@lua@class "):
name = header[0].split(" ", 1)[1]
classname = name.split(":")[0].strip()
printmsg(path, reader.line_no(), f"class {classname}")
if header_comment:
out.write(f"--- {header_comment}\n")
out.write(f"---@class {name}\n")
# inline class
if len(header) > 1:
for field in header[1:]:
if not field.startswith("@lua@field "):
panic(
path,
reader.line_no(),
f"Invalid inline class exposure - all lines must be fields, got '{field}'",
)
out.write(f"---{field.removeprefix('@lua')}\n")
out.write("\n")
continue
# class definition
# save functions for later (print fields first)
funcs = []
for comment in reader.read_class_body():
if comment[-1].startswith("@exposed "):
funcs.append(comment)
continue
if len(comment) > 1 or not comment[0].startswith("@lua"):
continue
out.write(f"---{comment[0].removeprefix('@lua')}\n")
if funcs:
# only define global if there are functions on the class
out.write(f"{classname} = {{}}\n\n")
else:
out.write("\n")
# note the space difference from the branch above
out.write("--- " + line + "\n")
for func in funcs:
write_func(path, reader.line_no(), func, out)
continue
# global function
if header[-1].startswith("@exposed "):
write_func(path, reader.line_no(), doc_comment, out)
continue
with lua_meta.open("w") as output:
output.write(BOILERPLATE[1:]) # skip the newline after triple quote
process_file(lua_api_file, output)
if __name__ == "__main__":
with lua_meta.open("w") as output:
output.write(BOILERPLATE[1:]) # skip the newline after triple quote
read_file(lua_api_file, output)

View file

@ -503,6 +503,8 @@ set(SOURCE_FILES
util/IpcQueue.hpp
util/LayoutHelper.cpp
util/LayoutHelper.hpp
util/LoadPixmap.cpp
util/LoadPixmap.hpp
util/RapidjsonHelpers.cpp
util/RapidjsonHelpers.hpp
util/RatelimitBucket.cpp
@ -634,6 +636,8 @@ set(SOURCE_FILES
widgets/helper/EditableModelView.hpp
widgets/helper/EffectLabel.cpp
widgets/helper/EffectLabel.hpp
widgets/helper/IconDelegate.cpp
widgets/helper/IconDelegate.hpp
widgets/helper/InvisibleSizeGrip.cpp
widgets/helper/InvisibleSizeGrip.hpp
widgets/helper/NotebookButton.cpp
@ -642,8 +646,6 @@ set(SOURCE_FILES
widgets/helper/NotebookTab.hpp
widgets/helper/RegExpItemDelegate.cpp
widgets/helper/RegExpItemDelegate.hpp
widgets/helper/TrimRegExpValidator.cpp
widgets/helper/TrimRegExpValidator.hpp
widgets/helper/ResizingTextEdit.cpp
widgets/helper/ResizingTextEdit.hpp
widgets/helper/ScrollbarHighlight.cpp
@ -658,6 +660,11 @@ set(SOURCE_FILES
widgets/helper/TitlebarButton.hpp
widgets/helper/TitlebarButtons.cpp
widgets/helper/TitlebarButtons.hpp
widgets/helper/TrimRegExpValidator.cpp
widgets/helper/TrimRegExpValidator.hpp
widgets/layout/FlowLayout.cpp
widgets/layout/FlowLayout.hpp
widgets/listview/GenericItemDelegate.cpp
widgets/listview/GenericItemDelegate.hpp
@ -989,7 +996,6 @@ if (APPLE AND BUILD_APP)
PROPERTIES
MACOSX_BUNDLE_BUNDLE_NAME "Chatterino"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.chatterino"
MACOSX_BUNDLE_INFO_STRING "Chat client for Twitch"
MACOSX_BUNDLE_LONG_VERSION_STRING "${PROJECT_VERSION}"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
@ -1145,3 +1151,14 @@ if(NOT CHATTERINO_UPDATER)
message(STATUS "Disabling the updater.")
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC CHATTERINO_DISABLE_UPDATER)
endif()
if (DOXYGEN_FOUND)
message(STATUS "Doxygen found, adding doxygen target")
# output will be in docs/html
set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs")
doxygen_add_docs(
doxygen
${CMAKE_CURRENT_LIST_DIR}
)
endif ()

View file

@ -86,10 +86,6 @@ namespace {
QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
#endif
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true);
#endif
QApplication::setStyle(QStyleFactory::create("Fusion"));
#ifndef Q_OS_MAC

View file

@ -1,6 +1,7 @@
#pragma once
#include <magic_enum/magic_enum.hpp>
#include "util/QMagicEnum.hpp"
#include <pajlada/settings.hpp>
#include <QString>
@ -108,10 +109,7 @@ public:
template <typename T2>
EnumStringSetting<Enum> &operator=(Enum newValue)
{
std::string enumName(magic_enum::enum_name(newValue));
auto qEnumName = QString::fromStdString(enumName);
this->setValue(qEnumName.toLower());
this->setValue(qmagicenum::enumNameString(newValue).toLower());
return *this;
}
@ -130,8 +128,8 @@ public:
Enum getEnum()
{
return magic_enum::enum_cast<Enum>(this->getValue().toStdString(),
magic_enum::case_insensitive)
return qmagicenum::enumCast<Enum>(this->getValue(),
qmagicenum::CASE_INSENSITIVE)
.value_or(this->defaultValue);
}

View file

@ -8,8 +8,15 @@
#include <optional>
#include <string>
#define LINK_CHATTERINO_WIKI "https://wiki.chatterino.com"
#define LINK_CHATTERINO_DISCORD "https://discord.gg/7Y5AYhAK4z"
#define LINK_CHATTERINO_SOURCE "https://github.com/Chatterino/chatterino2"
namespace chatterino {
const inline auto TWITCH_PLAYER_URL =
QStringLiteral("https://player.twitch.tv/?channel=%1&parent=twitch.tv");
enum class HighlightState {
None,
Highlighted,

View file

@ -12,9 +12,28 @@ struct ParsedLink {
#else
using StringView = QStringRef;
#endif
/// The parsed protocol of the link. Can be empty.
///
/// https://www.forsen.tv/commands
/// ^------^
StringView protocol;
/// The parsed host of the link. Can not be empty.
///
/// https://www.forsen.tv/commands
/// ^-----------^
StringView host;
/// The remainder of the link. Can be empty.
///
/// https://www.forsen.tv/commands
/// ^-------^
StringView rest;
/// The original unparsed link.
///
/// https://www.forsen.tv/commands
/// ^----------------------------^
QString source;
};

View file

@ -165,12 +165,22 @@ public:
else
{
int vecRow = this->getVectorIndexFromModelIndex(row);
// TODO: This is only a safety-thing for when we modify data that's being modified right now.
// It should not be necessary, but it would require some rethinking about this surrounding logic
if (vecRow >= this->vector_->readOnly()->size())
{
return false;
}
this->vector_->removeAt(vecRow, this);
assert(this->rows_[row].original);
TVectorItem item = this->getItemFromRow(
this->rows_[row].items, this->rows_[row].original.value());
this->vector_->insert(item, vecRow, this);
QVector<int> roles = QVector<int>();
roles.append(role);
emit dataChanged(index, index, roles);
}
return true;

View file

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

View file

@ -4,19 +4,40 @@
namespace chatterino {
QThread NetworkManager::workerThread;
QNetworkAccessManager NetworkManager::accessManager;
QThread *NetworkManager::workerThread = nullptr;
QNetworkAccessManager *NetworkManager::accessManager = nullptr;
void NetworkManager::init()
{
NetworkManager::accessManager.moveToThread(&NetworkManager::workerThread);
NetworkManager::workerThread.start();
assert(!NetworkManager::workerThread);
assert(!NetworkManager::accessManager);
NetworkManager::workerThread = new QThread;
NetworkManager::workerThread->start();
NetworkManager::accessManager = new QNetworkAccessManager;
NetworkManager::accessManager->moveToThread(NetworkManager::workerThread);
}
void NetworkManager::deinit()
{
NetworkManager::workerThread.quit();
NetworkManager::workerThread.wait();
assert(NetworkManager::workerThread);
assert(NetworkManager::accessManager);
// delete the access manager first:
// - put the event on the worker thread
// - wait for it to process
NetworkManager::accessManager->deleteLater();
NetworkManager::accessManager = nullptr;
if (NetworkManager::workerThread)
{
NetworkManager::workerThread->quit();
NetworkManager::workerThread->wait();
}
NetworkManager::workerThread->deleteLater();
NetworkManager::workerThread = nullptr;
}
} // namespace chatterino

View file

@ -10,8 +10,8 @@ class NetworkManager : public QObject
Q_OBJECT
public:
static QThread workerThread;
static QNetworkAccessManager accessManager;
static QThread *workerThread;
static QNetworkAccessManager *accessManager;
static void init();
static void deinit();

View file

@ -9,6 +9,7 @@
#include "util/AbandonObject.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include "util/QMagicEnum.hpp"
#include <magic_enum/magic_enum.hpp>
#include <QCryptographicHash>
@ -48,7 +49,7 @@ void loadUncached(std::shared_ptr<NetworkData> &&data)
NetworkRequester requester;
auto *worker = new NetworkTask(std::move(data));
worker->moveToThread(&NetworkManager::workerThread);
worker->moveToThread(NetworkManager::workerThread);
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
&NetworkTask::run);
@ -181,11 +182,9 @@ void NetworkData::emitFinally()
});
}
QLatin1String NetworkData::typeString() const
QString NetworkData::typeString() const
{
auto view = magic_enum::enum_name<NetworkRequestType>(this->requestType);
return QLatin1String{view.data(),
static_cast<QLatin1String::size_type>(view.size())};
return qmagicenum::enumNameString(this->requestType);
}
void load(std::shared_ptr<NetworkData> &&data)

View file

@ -60,7 +60,7 @@ public:
void emitError(NetworkResult &&result);
void emitFinally();
QLatin1String typeString() const;
QString typeString() const;
private:
QString hash_;

View file

@ -67,7 +67,9 @@ const QByteArray &NetworkResult::getData() const
QString NetworkResult::formatError() const
{
if (this->status_)
// Print the status for errors that mirror HTTP status codes (=0 || >99)
if (this->status_ && (this->error_ == QNetworkReply::NoError ||
this->error_ > QNetworkReply::UnknownNetworkError))
{
return QString::number(*this->status_);
}
@ -77,6 +79,13 @@ QString NetworkResult::formatError() const
this->error_);
if (name == nullptr)
{
if (this->status_)
{
return QStringLiteral("unknown error (status: %1, error: %2)")
.arg(QString::number(*this->status_),
QString::number(this->error_));
}
return QStringLiteral("unknown error (%1)").arg(this->error_);
}
return name;

View file

@ -54,41 +54,41 @@ QNetworkReply *NetworkTask::createReply()
{
const auto &data = this->data_;
const auto &request = this->data_->request;
auto &accessManager = NetworkManager::accessManager;
auto *accessManager = NetworkManager::accessManager;
switch (this->data_->requestType)
{
case NetworkRequestType::Get:
return accessManager.get(request);
return accessManager->get(request);
case NetworkRequestType::Put:
return accessManager.put(request, data->payload);
return accessManager->put(request, data->payload);
case NetworkRequestType::Delete:
return accessManager.deleteResource(data->request);
return accessManager->deleteResource(data->request);
case NetworkRequestType::Post:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.post(request,
data->multiPartPayload.get());
return accessManager->post(request,
data->multiPartPayload.get());
}
else
{
return accessManager.post(request, data->payload);
return accessManager->post(request, data->payload);
}
case NetworkRequestType::Patch:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.sendCustomRequest(
return accessManager->sendCustomRequest(
request, "PATCH", data->multiPartPayload.get());
}
else
{
return NetworkManager::accessManager.sendCustomRequest(
return NetworkManager::accessManager->sendCustomRequest(
request, "PATCH", data->payload);
}
}

View file

@ -401,6 +401,10 @@ void CommandController::initialize(Settings &, const Paths &paths)
this->registerCommand("/unmod", &commands::removeModerator);
this->registerCommand("/announce", &commands::sendAnnouncement);
this->registerCommand("/announceblue", &commands::sendAnnouncementBlue);
this->registerCommand("/announcegreen", &commands::sendAnnouncementGreen);
this->registerCommand("/announceorange", &commands::sendAnnouncementOrange);
this->registerCommand("/announcepurple", &commands::sendAnnouncementPurple);
this->registerCommand("/vip", &commands::addVIP);

View file

@ -9,9 +9,11 @@
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino::commands {
namespace {
using namespace chatterino;
QString sendAnnouncement(const CommandContext &ctx)
QString sendAnnouncementColor(const CommandContext &ctx,
const HelixAnnouncementColor color)
{
if (ctx.channel == nullptr)
{
@ -25,11 +27,28 @@ QString sendAnnouncement(const CommandContext &ctx)
return "";
}
QString colorStr = "";
if (color != HelixAnnouncementColor::Primary)
{
colorStr = qmagicenum::enumNameString(color).toLower();
}
if (ctx.words.size() < 2)
{
ctx.channel->addMessage(makeSystemMessage(
"Usage: /announce <message> - Call attention to your "
"message with a highlight."));
QString usageMsg;
if (color == HelixAnnouncementColor::Primary)
{
usageMsg = "Usage: /announce <message> - Call attention to your "
"message with a highlight.";
}
else
{
usageMsg =
QString("Usage: /announce%1 <message> - Call attention to your "
"message with a %1 highlight.")
.arg(colorStr);
}
ctx.channel->addMessage(makeSystemMessage(usageMsg));
return "";
}
@ -37,13 +56,14 @@ QString sendAnnouncement(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /announce command."));
QString("You must be logged in to use the /announce%1 command.")
.arg(colorStr)));
return "";
}
getHelix()->sendChatAnnouncement(
ctx.twitchChannel->roomId(), user->getUserId(),
ctx.words.mid(1).join(" "), HelixAnnouncementColor::Primary,
ctx.words.mid(1).join(" "), color,
[]() {
// do nothing.
},
@ -78,4 +98,33 @@ QString sendAnnouncement(const CommandContext &ctx)
return "";
}
} // namespace
namespace chatterino::commands {
QString sendAnnouncement(const CommandContext &ctx)
{
return sendAnnouncementColor(ctx, HelixAnnouncementColor::Primary);
}
QString sendAnnouncementBlue(const CommandContext &ctx)
{
return sendAnnouncementColor(ctx, HelixAnnouncementColor::Blue);
}
QString sendAnnouncementGreen(const CommandContext &ctx)
{
return sendAnnouncementColor(ctx, HelixAnnouncementColor::Green);
}
QString sendAnnouncementOrange(const CommandContext &ctx)
{
return sendAnnouncementColor(ctx, HelixAnnouncementColor::Orange);
}
QString sendAnnouncementPurple(const CommandContext &ctx)
{
return sendAnnouncementColor(ctx, HelixAnnouncementColor::Purple);
}
} // namespace chatterino::commands

View file

@ -13,4 +13,16 @@ namespace chatterino::commands {
/// /announce
QString sendAnnouncement(const CommandContext &ctx);
/// /announceblue
QString sendAnnouncementBlue(const CommandContext &ctx);
/// /announcegreen
QString sendAnnouncementGreen(const CommandContext &ctx);
/// /announceorange
QString sendAnnouncementOrange(const CommandContext &ctx);
/// /announcepurple
QString sendAnnouncementPurple(const CommandContext &ctx);
} // namespace chatterino::commands

View file

@ -50,6 +50,9 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
* message.content
* message.length
*
* reward.title
* reward.cost
* reward.id
*/
using MessageFlag = chatterino::MessageFlag;
@ -120,6 +123,18 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
vars["channel.live"] = false;
}
}
if (m->reward != nullptr)
{
vars["reward.title"] = m->reward->title;
vars["reward.cost"] = m->reward->cost;
vars["reward.id"] = m->reward->id;
}
else
{
vars["reward.title"] = "";
vars["reward.cost"] = -1;
vars["reward.id"] = "";
}
return vars;
}

View file

@ -48,6 +48,9 @@ static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
{"flags.monitored", Type::Bool},
{"message.content", Type::String},
{"message.length", Type::Int},
{"reward.title", Type::String},
{"reward.cost", Type::Int},
{"reward.id", Type::String},
};
ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel);

View file

@ -35,7 +35,11 @@ static const QMap<QString, QString> validIdentifiersMap = {
{"flags.restricted", "restricted message?"},
{"flags.monitored", "monitored message?"},
{"message.content", "message text"},
{"message.length", "message length"}};
{"message.length", "message length"},
{"reward.title", "point reward title"},
{"reward.cost", "point reward cost"},
{"reward.id", "point reward id"},
};
// clang-format off
static const QRegularExpression tokenRegex(

View file

@ -6,28 +6,11 @@
#include "singletons/Resources.hpp"
#include <QRegularExpression>
#include <QUrl>
namespace chatterino {
// ModerationAction::ModerationAction(Image *_image, const QString &_action)
// : _isImage(true)
// , image(_image)
// , action(_action)
//{
//}
// ModerationAction::ModerationAction(const QString &_line1, const QString
// &_line2,
// const QString &_action)
// : _isImage(false)
// , image(nullptr)
// , line1(_line1)
// , line2(_line2)
// , action(_action)
//{
//}
ModerationAction::ModerationAction(const QString &action)
ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath)
: action_(action)
{
static QRegularExpression replaceRegex("[!/.]");
@ -37,6 +20,8 @@ ModerationAction::ModerationAction(const QString &action)
if (timeoutMatch.hasMatch())
{
this->type_ = Type::Timeout;
// if (multipleTimeouts > 1) {
// QString line1;
// QString line2;
@ -99,24 +84,19 @@ ModerationAction::ModerationAction(const QString &action)
}
this->line2_ = "w";
}
// line1 = this->line1_;
// line2 = this->line2_;
// } else {
// this->_moderationActions.emplace_back(getResources().buttonTimeout,
// str);
// }
}
else if (action.startsWith("/ban "))
{
this->imageToLoad_ = 1;
this->type_ = Type::Ban;
}
else if (action.startsWith("/delete "))
{
this->imageToLoad_ = 2;
this->type_ = Type::Delete;
}
else
{
this->type_ = Type::Custom;
QString xD = action;
xD.replace(replaceRegex, "");
@ -124,6 +104,11 @@ ModerationAction::ModerationAction(const QString &action)
this->line1_ = xD.mid(0, 2);
this->line2_ = xD.mid(2, 2);
}
if (iconPath.isValid())
{
this->iconPath_ = iconPath;
}
}
bool ModerationAction::operator==(const ModerationAction &other) const
@ -139,19 +124,23 @@ bool ModerationAction::isImage() const
const std::optional<ImagePtr> &ModerationAction::getImage() const
{
assertInGuiThread();
if (this->imageToLoad_ != 0)
if (this->image_.has_value())
{
if (this->imageToLoad_ == 1)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->imageToLoad_ == 2)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
return this->image_;
}
if (this->iconPath_.isValid())
{
this->image_ = Image::fromUrl({this->iconPath_.toString()});
}
else if (this->type_ == Type::Ban)
{
this->image_ = Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->type_ == Type::Delete)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
return this->image_;
@ -172,4 +161,14 @@ const QString &ModerationAction::getAction() const
return this->action_;
}
const QUrl &ModerationAction::iconPath() const
{
return this->iconPath_;
}
ModerationAction::Type ModerationAction::getType() const
{
return this->type_;
}
} // namespace chatterino

View file

@ -4,6 +4,7 @@
#include <pajlada/serialize.hpp>
#include <QString>
#include <QUrl>
#include <memory>
#include <optional>
@ -16,7 +17,32 @@ using ImagePtr = std::shared_ptr<Image>;
class ModerationAction
{
public:
ModerationAction(const QString &action);
/**
* Type of the action, parsed from the input `action`
*/
enum class Type {
/**
* /ban <user>
*/
Ban,
/**
* /delete <msg-id>
*/
Delete,
/**
* /timeout <user> <duration>
*/
Timeout,
/**
* Anything not matching the action types above
*/
Custom,
};
ModerationAction(const QString &action, const QUrl &iconPath = {});
bool operator==(const ModerationAction &other) const;
@ -25,13 +51,18 @@ public:
const QString &getLine1() const;
const QString &getLine2() const;
const QString &getAction() const;
const QUrl &iconPath() const;
Type getType() const;
private:
mutable std::optional<ImagePtr> image_;
QString line1_;
QString line2_;
QString action_;
int imageToLoad_{};
Type type_{};
QUrl iconPath_;
};
} // namespace chatterino
@ -46,6 +77,7 @@ struct Serialize<chatterino::ModerationAction> {
rapidjson::Value ret(rapidjson::kObjectType);
chatterino::rj::set(ret, "pattern", value.getAction(), a);
chatterino::rj::set(ret, "icon", value.iconPath().toString(), a);
return ret;
}
@ -63,10 +95,12 @@ struct Deserialize<chatterino::ModerationAction> {
}
QString pattern;
chatterino::rj::getSafe(value, "pattern", pattern);
return chatterino::ModerationAction(pattern);
QString icon;
chatterino::rj::getSafe(value, "icon", icon);
return chatterino::ModerationAction(pattern, QUrl(icon));
}
};

View file

@ -1,13 +1,19 @@
#include "controllers/moderationactions/ModerationActionModel.hpp"
#include "controllers/moderationactions/ModerationAction.hpp"
#include "messages/Image.hpp"
#include "util/LoadPixmap.hpp"
#include "util/PostToThread.hpp"
#include "util/StandardItemHelper.hpp"
#include <QIcon>
#include <QPixmap>
namespace chatterino {
// commandmodel
ModerationActionModel ::ModerationActionModel(QObject *parent)
: SignalVectorModel<ModerationAction>(1, parent)
: SignalVectorModel<ModerationAction>(2, parent)
{
}
@ -15,14 +21,31 @@ ModerationActionModel ::ModerationActionModel(QObject *parent)
ModerationAction ModerationActionModel::getItemFromRow(
std::vector<QStandardItem *> &row, const ModerationAction &original)
{
return ModerationAction(row[0]->data(Qt::DisplayRole).toString());
return ModerationAction(
row[Column::Command]->data(Qt::DisplayRole).toString(),
row[Column::Icon]->data(Qt::UserRole).toString());
}
// turns a row in the model into a vector item
void ModerationActionModel::getRowFromItem(const ModerationAction &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item.getAction());
setStringItem(row[Column::Command], item.getAction());
setFilePathItem(row[Column::Icon], item.iconPath());
if (!item.iconPath().isEmpty())
{
auto oImage = item.getImage();
assert(oImage.has_value());
if (oImage.has_value())
{
auto url = oImage->get()->url();
loadPixmapFromUrl(url, [row](const QPixmap &pixmap) {
postToThread([row, pixmap]() {
row[Column::Icon]->setData(pixmap, Qt::DecorationRole);
});
});
}
}
}
} // namespace chatterino

View file

@ -13,6 +13,11 @@ class ModerationActionModel : public SignalVectorModel<ModerationAction>
public:
explicit ModerationActionModel(QObject *parent);
enum Column {
Command = 0,
Icon = 1,
};
protected:
// turn a vector item into a model row
ModerationAction getItemFromRow(std::vector<QStandardItem *> &row,

View file

@ -9,9 +9,11 @@
# include "messages/MessageBuilder.hpp"
# include "providers/twitch/TwitchIrcServer.hpp"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
# include <lualib.h>
}
# include <QFileInfo>
# include <QLoggingCategory>
# include <QTextCodec>
@ -117,9 +119,12 @@ int c2_register_callback(lua_State *L)
return 0;
}
auto callbackSavedName = QString("c2cb-%1").arg(
magic_enum::enum_name<EventType>(evtType).data());
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.toStdString().c_str());
auto typeName = magic_enum::enum_name(evtType);
std::string callbackSavedName;
callbackSavedName.reserve(5 + typeName.size());
callbackSavedName += "c2cb-";
callbackSavedName += typeName;
lua_setfield(L, LUA_REGISTRYINDEX, callbackSavedName.c_str());
lua_pop(L, 2);

View file

@ -2,7 +2,11 @@
#ifdef CHATTERINO_HAVE_PLUGINS
extern "C" {
# include <lua.h>
}
# include "controllers/plugins/LuaUtilities.hpp"
# include <QString>
# include <cassert>
@ -53,6 +57,28 @@ struct CompletionList {
bool hideOthers{};
};
/**
* @lua@class CompletionEvent
*/
struct CompletionEvent {
/**
* @lua@field query string The word being completed
*/
QString query;
/**
* @lua@field full_text_content string Content of the text input
*/
QString full_text_content;
/**
* @lua@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes)
*/
int cursor_position{};
/**
* @lua@field is_first_word boolean True if this is the first word in the input
*/
bool is_first_word{};
};
/**
* @includefile common/Channel.hpp
* @includefile controllers/plugins/api/ChannelRef.hpp
@ -72,7 +98,7 @@ int c2_register_command(lua_State *L);
* Registers a callback to be invoked when completions for a term are requested.
*
* @lua@param type "CompletionRequested"
* @lua@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
* @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked.
* @exposed c2.register_callback
*/
int c2_register_callback(lua_State *L);
@ -80,7 +106,7 @@ int c2_register_callback(lua_State *L);
/**
* Writes a message to the Chatterino log.
*
* @lua@param level LogLevel The desired level.
* @lua@param level c2.LogLevel The desired level.
* @lua@param ... any Values to log. Should be convertible to a string with `tostring()`.
* @exposed c2.log
*/

View file

@ -7,8 +7,10 @@
# include "controllers/plugins/api/ChannelRef.hpp"
# include "controllers/plugins/LuaAPI.hpp"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
}
# include <climits>
# include <cstdlib>
@ -140,6 +142,20 @@ StackIdx push(lua_State *L, const int &b)
return lua_gettop(L);
}
StackIdx push(lua_State *L, const api::CompletionEvent &ev)
{
auto idx = pushEmptyTable(L, 4);
# define PUSH(field) \
lua::push(L, ev.field); \
lua_setfield(L, idx, #field)
PUSH(query);
PUSH(full_text_content);
PUSH(cursor_position);
PUSH(is_first_word);
# undef PUSH
return idx;
}
bool peek(lua_State *L, int *out, StackIdx idx)
{
StackGuard guard(L);

View file

@ -4,8 +4,10 @@
# include "common/QLogging.hpp"
extern "C" {
# include <lua.h>
# include <lualib.h>
}
# include <magic_enum/magic_enum.hpp>
# include <QList>
@ -26,6 +28,7 @@ namespace chatterino::lua {
namespace api {
struct CompletionList;
struct CompletionEvent;
} // namespace api
constexpr int ERROR_BAD_PEEK = LUA_OK - 1;
@ -64,6 +67,7 @@ StackIdx push(lua_State *L, const QString &str);
StackIdx push(lua_State *L, const std::string &str);
StackIdx push(lua_State *L, const bool &b);
StackIdx push(lua_State *L, const int &b);
StackIdx push(lua_State *L, const api::CompletionEvent &ev);
// returns OK?
bool peek(lua_State *L, int *out, StackIdx idx = -1);

View file

@ -3,8 +3,11 @@
# include "common/QLogging.hpp"
# include "controllers/commands/CommandController.hpp"
# include "util/QMagicEnum.hpp"
extern "C" {
# include <lua.h>
}
# include <magic_enum/magic_enum.hpp>
# include <QJsonArray>
# include <QJsonObject>
@ -24,7 +27,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else if (!homepageObj.isUndefined())
{
QString type = magic_enum::enum_name(homepageObj.type()).data();
auto type = qmagicenum::enumName(homepageObj.type());
this->errors.emplace_back(
QString("homepage is defined but is not a string (its type is %1)")
.arg(type));
@ -36,7 +39,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else
{
QString type = magic_enum::enum_name(nameObj.type()).data();
auto type = qmagicenum::enumName(nameObj.type());
this->errors.emplace_back(
QString("name is not a string (its type is %1)").arg(type));
}
@ -48,7 +51,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else
{
QString type = magic_enum::enum_name(descrObj.type()).data();
auto type = qmagicenum::enumName(descrObj.type());
this->errors.emplace_back(
QString("description is not a string (its type is %1)").arg(type));
}
@ -62,7 +65,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
const auto &t = authorsArr.at(i);
if (!t.isString())
{
QString type = magic_enum::enum_name(t.type()).data();
auto type = qmagicenum::enumName(t.type());
this->errors.push_back(
QString("authors element #%1 is not a string (it is a %2)")
.arg(i)
@ -74,7 +77,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else
{
QString type = magic_enum::enum_name(authorsObj.type()).data();
auto type = qmagicenum::enumName(authorsObj.type());
this->errors.emplace_back(
QString("authors is not an array (its type is %1)").arg(type));
}
@ -86,7 +89,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else
{
QString type = magic_enum::enum_name(licenseObj.type()).data();
auto type = qmagicenum::enumName(licenseObj.type());
this->errors.emplace_back(
QString("license is not a string (its type is %1)").arg(type));
}
@ -107,7 +110,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
}
else
{
QString type = magic_enum::enum_name(verObj.type()).data();
auto type = qmagicenum::enumName(verObj.type());
this->errors.emplace_back(
QString("version is not a string (its type is %1)").arg(type));
this->version = semver::version(0, 0, 0);
@ -117,7 +120,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
{
if (!permsObj.isArray())
{
QString type = magic_enum::enum_name(permsObj.type()).data();
auto type = qmagicenum::enumName(permsObj.type());
this->errors.emplace_back(
QString("permissions is not an array (its type is %1)")
.arg(type));
@ -130,7 +133,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
const auto &t = permsArr.at(i);
if (!t.isObject())
{
QString type = magic_enum::enum_name(t.type()).data();
auto type = qmagicenum::enumName(t.type());
this->errors.push_back(QString("permissions element #%1 is not "
"an object (its type is %2)")
.arg(i)
@ -159,7 +162,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
{
if (!tagsObj.isArray())
{
QString type = magic_enum::enum_name(tagsObj.type()).data();
auto type = qmagicenum::enumName(tagsObj.type());
this->errors.emplace_back(
QString("tags is not an array (its type is %1)").arg(type));
return;
@ -171,7 +174,7 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
const auto &t = tagsArr.at(i);
if (!t.isString())
{
QString type = magic_enum::enum_name(t.type()).data();
auto type = qmagicenum::enumName(t.type());
this->errors.push_back(
QString("tags element #%1 is not a string (its type is %2)")
.arg(i)

View file

@ -98,8 +98,8 @@ public:
// Note: The CallbackFunction object's destructor will remove the function from the lua stack
using LuaCompletionCallback =
lua::CallbackFunction<lua::api::CompletionList, QString, QString, int,
bool>;
lua::CallbackFunction<lua::api::CompletionList,
lua::api::CompletionEvent>;
std::optional<LuaCompletionCallback> getCompletionCallback()
{
if (this->state_ == nullptr || !this->error_.isNull())
@ -107,14 +107,14 @@ public:
return {};
}
// this uses magic enum to help automatic tooling find usages
auto typeName =
magic_enum::enum_name(lua::api::EventType::CompletionRequested);
std::string cbName;
cbName.reserve(5 + typeName.size());
cbName += "c2cb-";
cbName += typeName;
auto typ =
lua_getfield(this->state_, LUA_REGISTRYINDEX,
QString("c2cb-%1")
.arg(magic_enum::enum_name<lua::api::EventType>(
lua::api::EventType::CompletionRequested)
.data())
.toStdString()
.c_str());
lua_getfield(this->state_, LUA_REGISTRYINDEX, cbName.c_str());
if (typ != LUA_TFUNCTION)
{
lua_pop(this->state_, 1);
@ -123,7 +123,7 @@ public:
// move
return std::make_optional<lua::CallbackFunction<
lua::api::CompletionList, QString, QString, int, bool>>(
lua::api::CompletionList, lua::api::CompletionEvent>>(
this->state_, lua_gettop(this->state_));
}

View file

@ -14,9 +14,11 @@
# include "singletons/Paths.hpp"
# include "singletons/Settings.hpp"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
# include <lualib.h>
}
# include <QJsonDocument>
# include <memory>
@ -431,8 +433,12 @@ std::pair<bool, QStringList> PluginController::updateCustomCompletions(
qCDebug(chatterinoLua)
<< "Processing custom completions from plugin" << name;
auto &cb = *opt;
auto errOrList =
cb(query, fullTextContent, cursorPosition, isFirstWord);
auto errOrList = cb(lua::api::CompletionEvent{
.query = query,
.full_text_content = fullTextContent,
.cursor_position = cursorPosition,
.is_first_word = isFirstWord,
});
if (std::holds_alternative<int>(errOrList))
{
guard.handled();

View file

@ -1,6 +1,8 @@
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/PluginPermission.hpp"
# include "util/QMagicEnum.hpp"
# include <magic_enum/magic_enum.hpp>
# include <QJsonObject>
@ -11,14 +13,13 @@ PluginPermission::PluginPermission(const QJsonObject &obj)
auto jsontype = obj.value("type");
if (!jsontype.isString())
{
QString tn = magic_enum::enum_name(jsontype.type()).data();
auto tn = qmagicenum::enumName(jsontype.type());
this->errors.emplace_back(QString("permission type is defined but is "
"not a string (its type is %1)")
.arg(tn));
}
auto strtype = jsontype.toString().toStdString();
auto opt = magic_enum::enum_cast<PluginPermission::Type>(
strtype, magic_enum::case_insensitive);
auto opt = qmagicenum::enumCast<PluginPermission::Type>(
jsontype.toString(), qmagicenum::CASE_INSENSITIVE);
if (!opt.has_value())
{
this->errors.emplace_back(QString("permission type is an unknown (%1)")

View file

@ -9,8 +9,10 @@
# include "providers/twitch/TwitchChannel.hpp"
# include "providers/twitch/TwitchIrcServer.hpp"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
}
# include <cassert>
# include <memory>

View file

@ -21,7 +21,7 @@ enum class LPlatform {
};
/**
* @lua@class Channel: IWeakResource
* @lua@class Channel
*/
struct ChannelRef {
static void createMetatable(lua_State *L);
@ -100,7 +100,7 @@ public:
* Compares the channel Type. Note that enum values aren't guaranteed, just
* that they are equal to the exposed enum.
*
* @lua@return bool
* @lua@return boolean
* @exposed Channel:is_twitch_channel
*/
static int is_twitch_channel(lua_State *L);
@ -193,7 +193,7 @@ public:
/**
* Finds a channel by the Twitch user ID of its owner.
*
* @lua@param string id ID of the owner of the channel.
* @lua@param id string ID of the owner of the channel.
* @lua@return Channel?
* @exposed Channel.by_twitch_id
*/
@ -216,13 +216,12 @@ struct LuaRoomModes {
bool subscriber_only = false;
/**
* @lua@field emotes_only boolean Whether or not text is allowed in messages.
* Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
* @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes
*/
bool emotes_only = false;
/**
* @lua@field unique_chat number? Time in minutes you need to follow to chat or nil.
* @lua@field follower_only number? Time in minutes you need to follow to chat or nil.
*/
std::optional<int> follower_only;
/**

View file

@ -4,8 +4,11 @@
# include "Application.hpp"
# include "controllers/plugins/LuaUtilities.hpp"
# include "controllers/plugins/PluginController.hpp"
# include "lauxlib.h"
# include "lua.h"
extern "C" {
# include <lauxlib.h>
# include <lua.h>
}
# include <cerrno>

View file

@ -26,11 +26,6 @@ using namespace chatterino;
int main(int argc, char **argv)
{
// TODO: This is a temporary fix (see #4552).
#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qputenv("QT_ENABLE_HIGHDPI_SCALING", "0");
#endif
QApplication a(argc, argv);
QCoreApplication::setApplicationName("chatterino");

View file

@ -24,14 +24,6 @@ public:
private:
/// Property Accessors
/**
* @brief Return the limit of the internal buffer
*/
[[nodiscard]] size_t limit() const
{
return this->limit_;
}
/**
* @brief Return the amount of space left in the buffer
*
@ -43,6 +35,14 @@ private:
}
public:
/**
* @brief Return the limit of the queue
*/
[[nodiscard]] size_t limit() const
{
return this->limit_;
}
/**
* @brief Return true if the buffer is empty
*/

View file

@ -1,6 +1,7 @@
#pragma once
#include "common/FlagsEnum.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "util/QStringHash.hpp"
#include <magic_enum/magic_enum.hpp>
@ -107,6 +108,8 @@ struct Message {
std::vector<std::unique_ptr<MessageElement>> elements;
ScrollbarHighlight getScrollBarHighlight() const;
std::shared_ptr<ChannelPointReward> reward = nullptr;
};
} // namespace chatterino

View file

@ -543,7 +543,7 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
// This also ensures that the LinkResolver doesn't get these links.
addText(imageLink, MessageColor::Link)
->setLink({Link::Url, imageLink})
->setTrailingSpace(false);
->setTrailingSpace(!deletionLink.isEmpty());
if (!deletionLink.isEmpty())
{
@ -617,16 +617,16 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink)
{
QString lowercaseLinkString;
QString origLink = parsedLink.source;
QString matchedLink;
QString fullUrl;
if (parsedLink.protocol.isNull())
{
matchedLink = QStringLiteral("http://") + parsedLink.source;
fullUrl = QStringLiteral("http://") + parsedLink.source;
}
else
{
lowercaseLinkString += parsedLink.protocol;
matchedLink = parsedLink.source;
fullUrl = parsedLink.source;
}
lowercaseLinkString += parsedLink.host.toString().toLower();
@ -635,9 +635,8 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink)
auto textColor = MessageColor(MessageColor::Link);
auto *el = this->emplace<LinkElement>(
LinkElement::Parsed{.lowercase = lowercaseLinkString,
.original = matchedLink},
MessageElementFlag::Text, textColor);
el->setLink({Link::Url, matchedLink});
.original = origLink},
fullUrl, MessageElementFlag::Text, textColor);
getIApp()->getLinkResolver()->resolve(el->linkInfo());
}
@ -764,10 +763,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_)
auto &&textColor = this->textColor_;
if (string.startsWith('@'))
{
this->emplace<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold);
this->emplace<TextElement>(string, MessageElementFlag::NonBoldUsername,
textColor);
this->emplace<MentionElement>(string, textColor, textColor);
}
else
{

View file

@ -155,8 +155,8 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
auto image = this->emote_->images.getImageOrLoaded(
container.getImageScale());
if (image->isEmpty())
{
return;
@ -210,7 +210,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto images = this->getLoadedImages(container.getScale());
auto images = this->getLoadedImages(container.getImageScale());
if (images.empty())
{
return;
@ -364,7 +364,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container,
if (flags.hasAny(this->getFlags()))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
this->emote_->images.getImageOrLoaded(container.getImageScale());
if (image->isEmpty())
{
return;
@ -454,7 +454,7 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags,
void TextElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
auto *app = getApp();
auto *app = getIApp();
if (flags.hasAny(this->getFlags()))
{
@ -463,6 +463,8 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
for (const auto &word : this->words_)
{
auto wordId = container.nextWordId();
auto getTextLayoutElement = [&](QString text, int width,
bool hasTrailingSpace) {
auto color = this->color_.getColor(*app->getThemes());
@ -473,6 +475,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
this->style_, container.getScale());
e->setTrailingSpace(hasTrailingSpace);
e->setText(text);
e->setWordId(wordId);
return e;
};
@ -676,10 +679,11 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
}
}
LinkElement::LinkElement(const Parsed &parsed, MessageElementFlags flags,
const MessageColor &color, FontStyle style)
LinkElement::LinkElement(const Parsed &parsed, const QString &fullUrl,
MessageElementFlags flags, const MessageColor &color,
FontStyle style)
: TextElement({}, flags, color, style)
, linkInfo_(parsed.original)
, linkInfo_(fullUrl)
, lowercase_({parsed.lowercase})
, original_({parsed.original})
{
@ -699,6 +703,38 @@ Link LinkElement::getLink() const
return {Link::Url, this->linkInfo_.url()};
}
MentionElement::MentionElement(const QString &name, MessageColor fallbackColor_,
MessageColor userColor_)
: TextElement(name, {MessageElementFlag::Text, MessageElementFlag::Mention})
, fallbackColor(fallbackColor_)
, userColor(userColor_)
{
}
void MentionElement::addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags)
{
if (getSettings()->colorUsernames)
{
this->color_ = this->userColor;
}
else
{
this->color_ = this->fallbackColor;
}
if (getSettings()->boldUsernames)
{
this->style_ = FontStyle::ChatMediumBold;
}
else
{
this->style_ = FontStyle::ChatMedium;
}
TextElement::addToContainer(container, flags);
}
// TIMESTAMP
TimestampElement::TimestampElement(QTime time)
: MessageElement(MessageElementFlag::Timestamp)
@ -794,7 +830,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
if (flags.hasAny(this->getFlags()))
{
const auto &image =
this->images_.getImageOrLoaded(container.getScale());
this->images_.getImageOrLoaded(container.getImageScale());
if (image->isEmpty())
{
return;

View file

@ -133,9 +133,10 @@ enum class MessageElementFlag : int64_t {
// needed
Collapsed = (1LL << 26),
// used for dynamic bold usernames
BoldUsername = (1LL << 27),
NonBoldUsername = (1LL << 28),
// A mention of a username that isn't the author of the message
Mention = (1LL << 27),
// Unused = (1LL << 28),
// used to check if links should be lowercased
LowercaseLinks = (1LL << 29),
@ -236,7 +237,6 @@ public:
protected:
QStringList words_;
private:
MessageColor color_;
FontStyle style_;
};
@ -272,7 +272,10 @@ public:
QString original;
};
LinkElement(const Parsed &parsed, MessageElementFlags flags,
/// @param parsed The link as it appeared in the message
/// @param fullUrl A full URL (notably with a protocol)
LinkElement(const Parsed &parsed, const QString &fullUrl,
MessageElementFlags flags,
const MessageColor &color = MessageColor::Text,
FontStyle style = FontStyle::ChatMedium);
~LinkElement() override = default;
@ -298,6 +301,42 @@ private:
QStringList original_;
};
/**
* @brief Contains a username mention.
*
* Examples of mentions:
* V
* 13:37 pajlada: hello @forsen
*
* V V
* 13:37 The moderators of this channel are: forsen, nuuls
*/
class MentionElement : public TextElement
{
public:
MentionElement(const QString &name, MessageColor fallbackColor_,
MessageColor userColor_);
~MentionElement() override = default;
MentionElement(const MentionElement &) = delete;
MentionElement(MentionElement &&) = delete;
MentionElement &operator=(const MentionElement &) = delete;
MentionElement &operator=(MentionElement &&) = delete;
void addToContainer(MessageLayoutContainer &container,
MessageElementFlags flags) override;
private:
/**
* The color of the element in case the "Colorize @usernames" is disabled
**/
MessageColor fallbackColor;
/**
* The color of the element in case the "Colorize @usernames" is enabled
**/
MessageColor userColor;
};
// contains emote data and will pick the emote based on :
// a) are images for the emote type enabled
// b) which size it wants

View file

@ -150,7 +150,7 @@ void SharedMessageBuilder::parseUsername()
void SharedMessageBuilder::parseHighlights()
{
if (getSettings()->isBlacklistedUser(this->ircMessage->nick()))
if (getSettings()->isBlacklistedUser(this->message().loginName))
{
// Do nothing. We ignore highlights from this user.
return;
@ -158,7 +158,7 @@ void SharedMessageBuilder::parseHighlights()
auto badges = SharedMessageBuilder::parseBadgeTag(this->tags);
auto [highlighted, highlightResult] = getIApp()->getHighlights()->check(
this->args, badges, this->ircMessage->nick(), this->originalMessage_,
this->args, badges, this->message().loginName, this->originalMessage_,
this->message().flags);
if (!highlighted)

View file

@ -74,7 +74,8 @@ int MessageLayout::getWidth() const
// Layout
// return true if redraw is required
bool MessageLayout::layout(int width, float scale, MessageElementFlags flags,
bool MessageLayout::layout(int width, float scale, float imageScale,
MessageElementFlags flags,
bool shouldInvalidateBuffer)
{
// BenchmarkGuard benchmark("MessageLayout::layout()");
@ -106,6 +107,8 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags,
// check if dpi changed
layoutRequired |= this->scale_ != scale;
this->scale_ = scale;
layoutRequired |= this->imageScale_ != imageScale;
this->imageScale_ = imageScale;
if (!layoutRequired)
{
@ -148,7 +151,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
bool hideSimilar = getSettings()->hideSimilar;
bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage);
this->container_.beginLayout(width, this->scale_, messageFlags);
this->container_.beginLayout(width, this->scale_, this->imageScale_,
messageFlags);
for (const auto &element : this->message_->elements)
{
@ -288,16 +292,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
}
// Create new buffer
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
this->buffer_ = std::make_unique<QPixmap>(
int(width * painter.device()->devicePixelRatioF()),
int(this->container_.getHeight() *
painter.device()->devicePixelRatioF()));
this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF());
#else
this->buffer_ = std::make_unique<QPixmap>(
width, std::max(16, this->container_.getHeight()));
#endif
this->bufferValid_ = false;
DebugCount::increase("message drawing buffers");
@ -443,12 +442,31 @@ void MessageLayout::deleteCache()
// returns nullptr if none was found
// fourtf: this should return a MessageLayoutItem
const MessageLayoutElement *MessageLayout::getElementAt(QPoint point)
const MessageLayoutElement *MessageLayout::getElementAt(QPoint point) const
{
// go through all words and return the first one that contains the point.
return this->container_.getElementAt(point);
}
std::pair<int, int> MessageLayout::getWordBounds(
const MessageLayoutElement *hoveredElement, QPoint relativePos) const
{
// An element with wordId != -1 can be multiline, so we need to check all
// elements in the container
if (hoveredElement->getWordId() != -1)
{
return this->container_.getWordBounds(hoveredElement);
}
const auto wordStart = this->getSelectionIndex(relativePos) -
hoveredElement->getMouseOverIndex(relativePos);
const auto selectionLength = hoveredElement->getSelectionIndexCount();
const auto length = hoveredElement->hasTrailingSpace() ? selectionLength - 1
: selectionLength;
return {wordStart, wordStart + length};
}
size_t MessageLayout::getLastCharacterIndex() const
{
return this->container_.getLastCharacterIndex();

View file

@ -56,8 +56,8 @@ public:
MessageLayoutFlags flags;
bool layout(int width, float scale_, MessageElementFlags flags,
bool shouldInvalidateBuffer);
bool layout(int width, float scale_, float imageScale,
MessageElementFlags flags, bool shouldInvalidateBuffer);
// Painting
MessagePaintResult paint(const MessagePaintContext &ctx);
@ -70,7 +70,21 @@ public:
*
* If no element is found at the given point, this returns a null pointer
*/
const MessageLayoutElement *getElementAt(QPoint point);
const MessageLayoutElement *getElementAt(QPoint point) const;
/**
* @brief Returns the word bounds of the given element
*
* The first value is the index of the first character in the word,
* the second value is the index of the character after the last character in the word.
*
* Given the word "abc" by itself, we would return (0, 3)
*
* V V
* "abc "
*/
std::pair<int, int> getWordBounds(
const MessageLayoutElement *hoveredElement, QPoint relativePos) const;
/**
* Get the index of the last character in this message's container
@ -114,6 +128,7 @@ private:
int currentLayoutWidth_ = -1;
int layoutState_ = -1;
float scale_ = -1;
float imageScale_ = -1.F;
MessageElementFlags currentWordFlags_;
#ifdef FOURTF

View file

@ -30,7 +30,7 @@ constexpr const QMargins MARGIN{8, 4, 8, 4};
namespace chatterino {
void MessageLayoutContainer::beginLayout(int width, float scale,
MessageFlags flags)
float imageScale, MessageFlags flags)
{
this->elements_.clear();
this->lines_.clear();
@ -45,12 +45,14 @@ void MessageLayoutContainer::beginLayout(int width, float scale,
this->width_ = width;
this->height_ = 0;
this->scale_ = scale;
this->imageScale_ = imageScale;
this->flags_ = flags;
auto mediumFontMetrics =
getIApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale);
this->textLineHeight_ = mediumFontMetrics.height();
this->spaceWidth_ = mediumFontMetrics.horizontalAdvance(' ');
this->dotdotdotWidth_ = mediumFontMetrics.horizontalAdvance("...");
this->currentWordId_ = 0;
this->canAddMessages_ = true;
this->isCollapsed_ = false;
this->wasPrevReversed_ = false;
@ -456,6 +458,50 @@ size_t MessageLayoutContainer::getFirstMessageCharacterIndex() const
return index;
}
std::pair<int, int> MessageLayoutContainer::getWordBounds(
const MessageLayoutElement *hoveredElement) const
{
if (this->elements_.empty())
{
return {0, 0};
}
size_t index = 0;
size_t wordStart = 0;
for (; index < this->elements_.size(); index++)
{
const auto &element = this->elements_[index];
if (element->getWordId() == hoveredElement->getWordId())
{
break;
}
wordStart += element->getSelectionIndexCount();
}
size_t wordEnd = wordStart;
for (; index < this->elements_.size(); index++)
{
const auto &element = this->elements_[index];
if (element->getWordId() != hoveredElement->getWordId())
{
break;
}
wordEnd += element->getSelectionIndexCount();
}
const auto *lastElementInSelection = this->elements_[index - 1].get();
if (lastElementInSelection->hasTrailingSpace())
{
wordEnd--;
}
return {wordStart, wordEnd};
}
size_t MessageLayoutContainer::getLastCharacterIndex() const
{
if (this->lines_.empty())
@ -481,6 +527,11 @@ float MessageLayoutContainer::getScale() const
return this->scale_;
}
float MessageLayoutContainer::getImageScale() const
{
return this->imageScale_;
}
bool MessageLayoutContainer::isCollapsed() const
{
return this->isCollapsed_;
@ -505,6 +556,11 @@ int MessageLayoutContainer::remainingWidth() const
this->currentX_;
}
int MessageLayoutContainer::nextWordId()
{
return this->currentWordId_++;
}
void MessageLayoutContainer::addElement(MessageLayoutElement *element,
const bool forceAdd,
const int prevIndex)
@ -700,9 +756,7 @@ void MessageLayoutContainer::reorderRTL(int firstTextIndex)
const auto neutral = isNeutral(element->getText());
const auto neutralOrUsername =
neutral ||
element->getFlags().hasAny({MessageElementFlag::BoldUsername,
MessageElementFlag::NonBoldUsername});
neutral || element->getFlags().has(MessageElementFlag::Mention);
if (neutral &&
((this->first == FirstWord::RTL && !this->wasPrevReversed_) ||

View file

@ -32,7 +32,8 @@ struct MessageLayoutContainer {
* This will reset all line calculations, and will be considered incomplete
* until the accompanying end function has been called
*/
void beginLayout(int width_, float scale_, MessageFlags flags_);
void beginLayout(int width, float scale, float imageScale,
MessageFlags flags);
/**
* Finish the layout process of this message
@ -111,6 +112,20 @@ struct MessageLayoutContainer {
*/
size_t getFirstMessageCharacterIndex() const;
/**
* @brief Returns the word bounds of the given element
*
* The first value is the index of the first character in the word,
* the second value is the index of the character after the last character in the word.
*
* Given the word "abc" by itself, we would return (0, 3)
*
* V V
* "abc "
*/
std::pair<int, int> getWordBounds(
const MessageLayoutElement *hoveredElement) const;
/**
* Get the index of the last character in this message
* This is the sum of all the characters in `elements_`
@ -132,6 +147,11 @@ struct MessageLayoutContainer {
*/
float getScale() const;
/**
* Returns the image scale
*/
float getImageScale() const;
/**
* Returns true if this message is collapsed
*/
@ -154,6 +174,11 @@ struct MessageLayoutContainer {
*/
int remainingWidth() const;
/**
* Returns the id of the next word that can be added to this container
*/
int nextWordId();
private:
struct Line {
/**
@ -251,6 +276,10 @@ private:
// variables
float scale_ = 1.F;
/**
* Scale factor for images
*/
float imageScale_ = 1.F;
int width_ = 0;
MessageFlags flags_{};
/**
@ -272,6 +301,7 @@ private:
int spaceWidth_ = 4;
int textLineHeight_ = 0;
int dotdotdotWidth_ = 0;
int currentWordId_ = 0;
bool canAddMessages_ = true;
bool isCollapsed_ = false;
bool wasPrevReversed_ = false;

View file

@ -108,6 +108,16 @@ FlagsEnum<MessageElementFlag> MessageLayoutElement::getFlags() const
return this->creator_.getFlags();
}
int MessageLayoutElement::getWordId() const
{
return this->wordId_;
}
void MessageLayoutElement::setWordId(int wordId)
{
this->wordId_ = wordId;
}
//
// IMAGE
//

View file

@ -71,6 +71,9 @@ public:
const QString &getText() const;
FlagsEnum<MessageElementFlag> getFlags() const;
int getWordId() const;
void setWordId(int wordId);
protected:
bool trailingSpace = true;
@ -83,6 +86,13 @@ private:
* The line of the container this element is laid out at
*/
size_t line_{};
/// @brief ID of a word inside its container
///
/// One word has exactly one ID that is used to identify elements created
/// from the same word (due to wrapping).
/// IDs are unique in a MessageLayoutContainer.
int wordId_ = -1;
};
// IMAGE

View file

@ -6,6 +6,7 @@
#include "providers/seventv/eventapi/Message.hpp"
#include "providers/seventv/SeventvBadges.hpp"
#include "providers/seventv/SeventvCosmetics.hpp"
#include "util/QMagicEnum.hpp"
#include <QJsonArray>
@ -228,7 +229,7 @@ void SeventvEventAPI::handleDispatch(const Dispatch &dispatch)
default: {
qCDebug(chatterinoSeventvEventAPI)
<< "Unknown subscription type:"
<< magic_enum::enum_name(dispatch.type).data()
<< qmagicenum::enumName(dispatch.type)
<< "body:" << dispatch.body;
}
break;

View file

@ -1,5 +1,7 @@
#include "providers/seventv/eventapi/Dispatch.hpp"
#include "util/QMagicEnum.hpp"
#include <QJsonArray>
#include <utility>
@ -7,8 +9,7 @@
namespace chatterino::seventv::eventapi {
Dispatch::Dispatch(QJsonObject obj)
: type(magic_enum::enum_cast<SubscriptionType>(
obj["type"].toString().toStdString())
: type(qmagicenum::enumCast<SubscriptionType>(obj["type"].toString())
.value_or(SubscriptionType::INVALID))
, body(obj["body"].toObject())
, id(this->body["id"].toString())
@ -95,8 +96,8 @@ bool UserConnectionUpdateDispatch::validate() const
CosmeticCreateDispatch::CosmeticCreateDispatch(const Dispatch &dispatch)
: data(dispatch.body["object"]["data"].toObject())
, kind(magic_enum::enum_cast<CosmeticKind>(
dispatch.body["object"]["kind"].toString().toStdString())
, kind(qmagicenum::enumCast<CosmeticKind>(
dispatch.body["object"]["kind"].toString())
.value_or(CosmeticKind::INVALID))
{
}
@ -111,8 +112,7 @@ EntitlementCreateDeleteDispatch::EntitlementCreateDeleteDispatch(
{
const auto obj = dispatch.body["object"].toObject();
this->refID = obj["ref_id"].toString();
this->kind = magic_enum::enum_cast<CosmeticKind>(
obj["kind"].toString().toStdString())
this->kind = qmagicenum::enumCast<CosmeticKind>(obj["kind"].toString())
.value_or(CosmeticKind::INVALID);
const auto userConnections = obj["user"]["connections"].toArray();

View file

@ -1,5 +1,7 @@
#include "providers/seventv/eventapi/Subscription.hpp"
#include "util/QMagicEnum.hpp"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
@ -9,14 +11,15 @@
namespace {
using namespace chatterino;
using namespace chatterino::seventv::eventapi;
const char *typeToString(SubscriptionType type)
QString typeToString(SubscriptionType type)
{
return magic_enum::enum_name(type).data();
return qmagicenum::enumNameString(type);
}
QJsonObject createDataJson(const char *typeName, const Condition &condition)
QJsonObject createDataJson(const QString &typeName, const Condition &condition)
{
QJsonObject data;
data["type"] = typeName;
@ -45,7 +48,7 @@ bool Subscription::operator!=(const Subscription &rhs) const
QByteArray Subscription::encodeSubscribe() const
{
const auto *typeName = typeToString(this->type);
auto typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)Opcode::Subscribe;
root["d"] = createDataJson(typeName, this->condition);
@ -54,7 +57,7 @@ QByteArray Subscription::encodeSubscribe() const
QByteArray Subscription::encodeUnsubscribe() const
{
const auto *typeName = typeToString(this->type);
auto typeName = typeToString(this->type);
QJsonObject root;
root["op"] = (int)Opcode::Unsubscribe;
root["d"] = createDataJson(typeName, this->condition);
@ -66,8 +69,7 @@ QDebug &operator<<(QDebug &dbg, const Subscription &subscription)
std::visit(
[&](const auto &cond) {
dbg << "Subscription{ condition:" << cond
<< "type:" << magic_enum::enum_name(subscription.type).data()
<< '}';
<< "type:" << qmagicenum::enumName(subscription.type) << '}';
},
subscription.condition);
return dbg;

View file

@ -53,6 +53,8 @@ const QSet<QString> SPECIAL_MESSAGE_TYPES{
"viewermilestone", // watch streak, but other categories possible in future
};
const QString ANONYMOUS_GIFTER_ID = "274598607";
MessagePtr generateBannedMessage(bool confirmedBan)
{
const auto linkColor = MessageColor(MessageColor::Link);
@ -516,6 +518,41 @@ std::vector<MessagePtr> parseUserNoticeMessage(Channel *channel,
{
messageText = "Announcement";
}
else if (msgType == "subgift")
{
if (auto monthsIt = tags.find("msg-param-gift-months");
monthsIt != tags.end())
{
int months = monthsIt.value().toInt();
if (months > 1)
{
auto plan = tags.value("msg-param-sub-plan").toString();
QString name =
ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()
? "An anonymous user"
: tags.value("display-name").toString();
messageText =
QString("%1 gifted %2 months of a Tier %3 sub to %4!")
.arg(name, QString::number(months),
plan.isEmpty() ? '1' : plan.at(0),
tags.value("msg-param-recipient-display-name")
.toString());
if (auto countIt = tags.find("msg-param-sender-count");
countIt != tags.end())
{
int count = countIt.value().toInt();
if (count > months)
{
messageText +=
QString(
" They've gifted %1 months in the channel.")
.arg(QString::number(count));
}
}
}
}
}
auto b = MessageBuilder(systemMessage, parseTagString(messageText),
calculateMessageTime(message).time());
@ -1010,6 +1047,41 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
{
messageText = "Announcement";
}
else if (msgType == "subgift")
{
if (auto monthsIt = tags.find("msg-param-gift-months");
monthsIt != tags.end())
{
int months = monthsIt.value().toInt();
if (months > 1)
{
auto plan = tags.value("msg-param-sub-plan").toString();
QString name =
ANONYMOUS_GIFTER_ID == tags.value("user-id").toString()
? "An anonymous user"
: tags.value("display-name").toString();
messageText =
QString("%1 gifted %2 months of a Tier %3 sub to %4!")
.arg(name, QString::number(months),
plan.isEmpty() ? '1' : plan.at(0),
tags.value("msg-param-recipient-display-name")
.toString());
if (auto countIt = tags.find("msg-param-sender-count");
countIt != tags.end())
{
int count = countIt.value().toInt();
if (count > months)
{
messageText +=
QString(
" They've gifted %1 months in the channel.")
.arg(QString::number(count));
}
}
}
}
}
auto b = MessageBuilder(systemMessage, parseTagString(messageText),
calculateMessageTime(message).time());

View file

@ -7,6 +7,7 @@
#include "messages/Image.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "util/DisplayBadge.hpp"
#include "util/LoadPixmap.hpp"
#include <QBuffer>
#include <QFile>
@ -239,48 +240,20 @@ void TwitchBadges::getBadgeIcons(const QList<DisplayBadge> &badges,
}
}
void TwitchBadges::loadEmoteImage(const QString &name, ImagePtr image,
void TwitchBadges::loadEmoteImage(const QString &name, const ImagePtr &image,
BadgeIconCallback &&callback)
{
auto url = image->url().string;
NetworkRequest(url)
.concurrent()
.cache()
.onSuccess([this, name, callback, url](auto result) {
auto data = result.getData();
loadPixmapFromUrl(image->url(),
[this, name, callback{std::move(callback)}](auto pixmap) {
auto icon = std::make_shared<QIcon>(pixmap);
// const cast since we are only reading from it
QBuffer buffer(const_cast<QByteArray *>(&data));
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
{
std::unique_lock lock(this->badgesMutex_);
this->badgesMap_[name] = icon;
}
if (!reader.canRead() || reader.size().isEmpty())
{
qCWarning(chatterinoTwitch)
<< "Can't read badge image at" << url << "for" << name
<< reader.errorString();
return;
}
QImage image = reader.read();
if (image.isNull())
{
qCWarning(chatterinoTwitch)
<< "Failed reading badge image at" << url << "for" << name
<< reader.errorString();
return;
}
auto icon = std::make_shared<QIcon>(QPixmap::fromImage(image));
{
std::unique_lock lock(this->badgesMutex_);
this->badgesMap_[name] = icon;
}
callback(name, icon);
})
.execute();
callback(name, icon);
});
}
} // namespace chatterino

View file

@ -48,7 +48,7 @@ public:
private:
void parseTwitchBadges(QJsonObject root);
void loaded();
void loadEmoteImage(const QString &name, ImagePtr image,
void loadEmoteImage(const QString &name, const ImagePtr &image,
BadgeIconCallback &&callback);
std::shared_mutex badgesMutex_;

View file

@ -84,8 +84,7 @@ TwitchChannel::TwitchChannel(const QString &name)
, nameOptions{name, name, name}
, subscriptionUrl_("https://www.twitch.tv/subs/" + name)
, channelUrl_("https://twitch.tv/" + name)
, popoutPlayerUrl_("https://player.twitch.tv/?parent=twitch.tv&channel=" +
name)
, popoutPlayerUrl_(TWITCH_PLAYER_URL.arg(name))
, bttvEmotes_(std::make_shared<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, seventvEmotes_(std::make_shared<EmoteMap>())

View file

@ -5,6 +5,400 @@
#include "messages/Image.hpp"
#include "util/QStringHash.hpp"
namespace {
using namespace chatterino;
Url getEmoteLink(const EmoteId &id, const QString &emoteScale)
{
return {QString(TWITCH_EMOTE_TEMPLATE)
.replace("{id}", id.string)
.replace("{scale}", emoteScale)};
}
QSize getEmoteExpectedBaseSize(const EmoteId &id)
{
// From Twitch docs - expected size for an emote (1x)
constexpr QSize defaultBaseSize(28, 28);
static std::unordered_map<QString, QSize> outliers{
{"555555635", {21, 18}}, /* ;p */
{"555555636", {21, 18}}, /* ;-p */
{"555555614", {21, 18}}, /* O_o */
{"555555641", {21, 18}}, /* :z */
{"555555604", {21, 18}}, /* :\\ */
{"444", {21, 18}}, /* :| */
{"555555634", {21, 18}}, /* ;-P */
{"439", {21, 18}}, /* ;) */
{"555555642", {21, 18}}, /* :-z */
{"555555613", {21, 18}}, /* :-o */
{"555555625", {21, 18}}, /* :-p */
{"433", {21, 18}}, /* :/ */
{"555555622", {21, 18}}, /* :P */
{"555555640", {21, 18}}, /* :-| */
{"555555623", {21, 18}}, /* :-P */
{"555555628", {21, 18}}, /* :) */
{"555555632", {21, 18}}, /* 8-) */
{"555555667", {20, 18}}, /* ;p */
{"445", {21, 18}}, /* <3 */
{"555555668", {20, 18}}, /* ;-p */
{"555555679", {20, 18}}, /* :z */
{"483", {20, 18}}, /* <3 */
{"555555666", {20, 18}}, /* ;-P */
{"497", {20, 18}}, /* O_o */
{"555555664", {20, 18}}, /* :-p */
{"555555671", {20, 18}}, /* :o */
{"555555681", {20, 18}}, /* :Z */
{"555555672", {20, 18}}, /* :-o */
{"555555676", {20, 18}}, /* :-\\ */
{"555555611", {21, 18}}, /* :-O */
{"555555670", {20, 18}}, /* :-O */
{"555555688", {20, 18}}, /* :-D */
{"441", {21, 18}}, /* B) */
{"555555601", {21, 18}}, /* >( */
{"491", {20, 18}}, /* ;P */
{"496", {20, 18}}, /* :D */
{"492", {20, 18}}, /* :O */
{"555555573", {24, 18}}, /* o_O */
{"555555643", {21, 18}}, /* :Z */
{"1898", {26, 28}}, /* ThunBeast */
{"555555682", {20, 18}}, /* :-Z */
{"1896", {20, 30}}, /* WholeWheat */
{"1906", {24, 30}}, /* SoBayed */
{"555555607", {21, 18}}, /* :-( */
{"555555660", {20, 18}}, /* :-( */
{"489", {20, 18}}, /* :( */
{"495", {20, 18}}, /* :s */
{"555555638", {21, 18}}, /* :-D */
{"357", {28, 30}}, /* HotPokket */
{"555555624", {21, 18}}, /* :p */
{"73", {21, 30}}, /* DBstyle */
{"555555674", {20, 18}}, /* :-/ */
{"555555629", {21, 18}}, /* :-) */
{"555555600", {24, 18}}, /* R-) */
{"41", {19, 27}}, /* Kreygasm */
{"555555612", {21, 18}}, /* :o */
{"488", {29, 24}}, /* :7 */
{"69", {41, 28}}, /* BloodTrail */
{"555555608", {21, 18}}, /* R) */
{"501", {20, 18}}, /* ;) */
{"50", {18, 27}}, /* ArsonNoSexy */
{"443", {21, 18}}, /* :D */
{"1904", {24, 30}}, /* BigBrother */
{"555555595", {24, 18}}, /* ;P */
{"555555663", {20, 18}}, /* :p */
{"555555576", {24, 18}}, /* o.o */
{"360", {22, 30}}, /* FailFish */
{"500", {20, 18}}, /* B) */
{"3", {24, 18}}, /* :D */
{"484", {20, 22}}, /* R) */
{"555555678", {20, 18}}, /* :-| */
{"7", {24, 18}}, /* B) */
{"52", {32, 32}}, /* SMOrc */
{"555555644", {21, 18}}, /* :-Z */
{"18", {20, 27}}, /* TheRinger */
{"49106", {27, 28}}, /* CorgiDerp */
{"6", {24, 18}}, /* O_o */
{"10", {24, 18}}, /* :/ */
{"47", {24, 24}}, /* PunchTrees */
{"555555561", {24, 18}}, /* :-D */
{"555555564", {24, 18}}, /* :-| */
{"13", {24, 18}}, /* ;P */
{"555555593", {24, 18}}, /* :p */
{"555555589", {24, 18}}, /* ;) */
{"555555590", {24, 18}}, /* ;-) */
{"486", {27, 42}}, /* :> */
{"40", {21, 27}}, /* KevinTurtle */
{"555555558", {24, 18}}, /* :( */
{"555555597", {24, 18}}, /* ;p */
{"555555580", {24, 18}}, /* :O */
{"555555567", {24, 18}}, /* :Z */
{"1", {24, 18}}, /* :) */
{"11", {24, 18}}, /* ;) */
{"33", {25, 32}}, /* DansGame */
{"555555586", {24, 18}}, /* :-/ */
{"4", {24, 18}}, /* >( */
{"555555588", {24, 18}}, /* :-\\ */
{"12", {24, 18}}, /* :P */
{"555555563", {24, 18}}, /* :| */
{"555555581", {24, 18}}, /* :-O */
{"555555598", {24, 18}}, /* ;-p */
{"555555596", {24, 18}}, /* ;-P */
{"555555557", {24, 18}}, /* :-) */
{"498", {20, 18}}, /* >( */
{"555555680", {20, 18}}, /* :-z */
{"555555587", {24, 18}}, /* :\\ */
{"5", {24, 18}}, /* :| */
{"354", {20, 30}}, /* 4Head */
{"555555562", {24, 18}}, /* >( */
{"555555594", {24, 18}}, /* :-p */
{"490", {20, 18}}, /* :P */
{"555555662", {20, 18}}, /* :-P */
{"2", {24, 18}}, /* :( */
{"1902", {27, 29}}, /* Keepo */
{"555555627", {21, 18}}, /* ;-) */
{"555555566", {24, 18}}, /* :-z */
{"555555559", {24, 18}}, /* :-( */
{"555555592", {24, 18}}, /* :-P */
{"28", {39, 27}}, /* MrDestructoid */
{"8", {24, 18}}, /* :O */
{"244", {24, 30}}, /* FUNgineer */
{"555555591", {24, 18}}, /* :P */
{"555555585", {24, 18}}, /* :/ */
{"494", {20, 18}}, /* :| */
{"9", {24, 18}}, /* <3 */
{"555555584", {24, 18}}, /* <3 */
{"555555579", {24, 18}}, /* 8-) */
{"14", {24, 18}}, /* R) */
{"485", {27, 18}}, /* #/ */
{"555555560", {24, 18}}, /* :D */
{"86", {36, 30}}, /* BibleThump */
{"555555578", {24, 18}}, /* B-) */
{"17", {20, 27}}, /* StoneLightning */
{"436", {21, 18}}, /* :O */
{"555555675", {20, 18}}, /* :\\ */
{"22", {19, 27}}, /* RedCoat */
{"555555574", {24, 18}}, /* o.O */
{"555555603", {21, 18}}, /* :-/ */
{"1901", {24, 28}}, /* Kippa */
{"15", {21, 27}}, /* JKanStyle */
{"555555605", {21, 18}}, /* :-\\ */
{"555555701", {20, 18}}, /* ;-) */
{"487", {20, 42}}, /* <] */
{"555555572", {24, 18}}, /* O.O */
{"65", {40, 30}}, /* FrankerZ */
{"25", {25, 28}}, /* Kappa */
{"36", {36, 30}}, /* PJSalt */
{"499", {20, 18}}, /* :) */
{"555555565", {24, 18}}, /* :z */
{"434", {21, 18}}, /* :( */
{"555555577", {24, 18}}, /* B) */
{"34", {21, 28}}, /* SwiftRage */
{"555555575", {24, 18}}, /* o_o */
{"92", {23, 30}}, /* PMSTwin */
{"555555570", {24, 18}}, /* O.o */
{"555555569", {24, 18}}, /* O_o */
{"493", {20, 18}}, /* :/ */
{"26", {20, 27}}, /* JonCarnage */
{"66", {20, 27}}, /* OneHand */
{"555555568", {24, 18}}, /* :-Z */
{"555555599", {24, 18}}, /* R) */
{"1900", {33, 30}}, /* RalpherZ */
{"555555582", {24, 18}}, /* :o */
{"1899", {22, 30}}, /* TF2John */
{"555555633", {21, 18}}, /* ;P */
{"16", {22, 27}}, /* OptimizePrime */
{"30", {29, 27}}, /* BCWarrior */
{"555555583", {24, 18}}, /* :-o */
{"32", {21, 27}}, /* GingerPower */
{"87", {24, 30}}, /* ShazBotstix */
{"74", {24, 30}}, /* AsianGlow */
{"555555571", {24, 18}}, /* O_O */
{"46", {24, 24}}, /* SSSsss */
};
auto it = outliers.find(id.string);
if (it != outliers.end())
{
return it->second;
}
return defaultBaseSize;
}
qreal getEmote3xScaleFactor(const EmoteId &id)
{
// From Twitch docs - expected size for an emote (1x)
constexpr qreal default3xScaleFactor = 0.25;
static std::unordered_map<QString, qreal> outliers{
{"555555635", 0.3333333333333333}, /* ;p */
{"555555636", 0.3333333333333333}, /* ;-p */
{"555555614", 0.3333333333333333}, /* O_o */
{"555555641", 0.3333333333333333}, /* :z */
{"555555604", 0.3333333333333333}, /* :\\ */
{"444", 0.3333333333333333}, /* :| */
{"555555634", 0.3333333333333333}, /* ;-P */
{"439", 0.3333333333333333}, /* ;) */
{"555555642", 0.3333333333333333}, /* :-z */
{"555555613", 0.3333333333333333}, /* :-o */
{"555555625", 0.3333333333333333}, /* :-p */
{"433", 0.3333333333333333}, /* :/ */
{"555555622", 0.3333333333333333}, /* :P */
{"555555640", 0.3333333333333333}, /* :-| */
{"555555623", 0.3333333333333333}, /* :-P */
{"555555628", 0.3333333333333333}, /* :) */
{"555555632", 0.3333333333333333}, /* 8-) */
{"555555667", 0.3333333333333333}, /* ;p */
{"445", 0.3333333333333333}, /* <3 */
{"555555668", 0.3333333333333333}, /* ;-p */
{"555555679", 0.3333333333333333}, /* :z */
{"483", 0.3333333333333333}, /* <3 */
{"555555666", 0.3333333333333333}, /* ;-P */
{"497", 0.3333333333333333}, /* O_o */
{"555555664", 0.3333333333333333}, /* :-p */
{"555555671", 0.3333333333333333}, /* :o */
{"555555681", 0.3333333333333333}, /* :Z */
{"555555672", 0.3333333333333333}, /* :-o */
{"555555676", 0.3333333333333333}, /* :-\\ */
{"555555611", 0.3333333333333333}, /* :-O */
{"555555670", 0.3333333333333333}, /* :-O */
{"555555688", 0.3333333333333333}, /* :-D */
{"441", 0.3333333333333333}, /* B) */
{"555555601", 0.3333333333333333}, /* >( */
{"491", 0.3333333333333333}, /* ;P */
{"496", 0.3333333333333333}, /* :D */
{"492", 0.3333333333333333}, /* :O */
{"555555573", 0.3333333333333333}, /* o_O */
{"555555643", 0.3333333333333333}, /* :Z */
{"1898", 0.3333333333333333}, /* ThunBeast */
{"555555682", 0.3333333333333333}, /* :-Z */
{"1896", 0.3333333333333333}, /* WholeWheat */
{"1906", 0.3333333333333333}, /* SoBayed */
{"555555607", 0.3333333333333333}, /* :-( */
{"555555660", 0.3333333333333333}, /* :-( */
{"489", 0.3333333333333333}, /* :( */
{"495", 0.3333333333333333}, /* :s */
{"555555638", 0.3333333333333333}, /* :-D */
{"357", 0.3333333333333333}, /* HotPokket */
{"555555624", 0.3333333333333333}, /* :p */
{"73", 0.3333333333333333}, /* DBstyle */
{"555555674", 0.3333333333333333}, /* :-/ */
{"555555629", 0.3333333333333333}, /* :-) */
{"555555600", 0.3333333333333333}, /* R-) */
{"41", 0.3333333333333333}, /* Kreygasm */
{"555555612", 0.3333333333333333}, /* :o */
{"488", 0.3333333333333333}, /* :7 */
{"69", 0.3333333333333333}, /* BloodTrail */
{"555555608", 0.3333333333333333}, /* R) */
{"501", 0.3333333333333333}, /* ;) */
{"50", 0.3333333333333333}, /* ArsonNoSexy */
{"443", 0.3333333333333333}, /* :D */
{"1904", 0.3333333333333333}, /* BigBrother */
{"555555595", 0.3333333333333333}, /* ;P */
{"555555663", 0.3333333333333333}, /* :p */
{"555555576", 0.3333333333333333}, /* o.o */
{"360", 0.3333333333333333}, /* FailFish */
{"500", 0.3333333333333333}, /* B) */
{"3", 0.3333333333333333}, /* :D */
{"484", 0.3333333333333333}, /* R) */
{"555555678", 0.3333333333333333}, /* :-| */
{"7", 0.3333333333333333}, /* B) */
{"52", 0.3333333333333333}, /* SMOrc */
{"555555644", 0.3333333333333333}, /* :-Z */
{"18", 0.3333333333333333}, /* TheRinger */
{"49106", 0.3333333333333333}, /* CorgiDerp */
{"6", 0.3333333333333333}, /* O_o */
{"10", 0.3333333333333333}, /* :/ */
{"47", 0.3333333333333333}, /* PunchTrees */
{"555555561", 0.3333333333333333}, /* :-D */
{"555555564", 0.3333333333333333}, /* :-| */
{"13", 0.3333333333333333}, /* ;P */
{"555555593", 0.3333333333333333}, /* :p */
{"555555589", 0.3333333333333333}, /* ;) */
{"555555590", 0.3333333333333333}, /* ;-) */
{"486", 0.3333333333333333}, /* :> */
{"40", 0.3333333333333333}, /* KevinTurtle */
{"555555558", 0.3333333333333333}, /* :( */
{"555555597", 0.3333333333333333}, /* ;p */
{"555555580", 0.3333333333333333}, /* :O */
{"555555567", 0.3333333333333333}, /* :Z */
{"1", 0.3333333333333333}, /* :) */
{"11", 0.3333333333333333}, /* ;) */
{"33", 0.3333333333333333}, /* DansGame */
{"555555586", 0.3333333333333333}, /* :-/ */
{"4", 0.3333333333333333}, /* >( */
{"555555588", 0.3333333333333333}, /* :-\\ */
{"12", 0.3333333333333333}, /* :P */
{"555555563", 0.3333333333333333}, /* :| */
{"555555581", 0.3333333333333333}, /* :-O */
{"555555598", 0.3333333333333333}, /* ;-p */
{"555555596", 0.3333333333333333}, /* ;-P */
{"555555557", 0.3333333333333333}, /* :-) */
{"498", 0.3333333333333333}, /* >( */
{"555555680", 0.3333333333333333}, /* :-z */
{"555555587", 0.3333333333333333}, /* :\\ */
{"5", 0.3333333333333333}, /* :| */
{"354", 0.3333333333333333}, /* 4Head */
{"555555562", 0.3333333333333333}, /* >( */
{"555555594", 0.3333333333333333}, /* :-p */
{"490", 0.3333333333333333}, /* :P */
{"555555662", 0.3333333333333333}, /* :-P */
{"2", 0.3333333333333333}, /* :( */
{"1902", 0.3333333333333333}, /* Keepo */
{"555555627", 0.3333333333333333}, /* ;-) */
{"555555566", 0.3333333333333333}, /* :-z */
{"555555559", 0.3333333333333333}, /* :-( */
{"555555592", 0.3333333333333333}, /* :-P */
{"28", 0.3333333333333333}, /* MrDestructoid */
{"8", 0.3333333333333333}, /* :O */
{"244", 0.3333333333333333}, /* FUNgineer */
{"555555591", 0.3333333333333333}, /* :P */
{"555555585", 0.3333333333333333}, /* :/ */
{"494", 0.3333333333333333}, /* :| */
{"9", 0.21428571428571427}, /* <3 */
{"555555584", 0.21428571428571427}, /* <3 */
{"555555579", 0.3333333333333333}, /* 8-) */
{"14", 0.3333333333333333}, /* R) */
{"485", 0.3333333333333333}, /* #/ */
{"555555560", 0.3333333333333333}, /* :D */
{"86", 0.3333333333333333}, /* BibleThump */
{"555555578", 0.3333333333333333}, /* B-) */
{"17", 0.3333333333333333}, /* StoneLightning */
{"436", 0.3333333333333333}, /* :O */
{"555555675", 0.3333333333333333}, /* :\\ */
{"22", 0.3333333333333333}, /* RedCoat */
{"245", 0.3333333333333333}, /* ResidentSleeper */
{"555555574", 0.3333333333333333}, /* o.O */
{"555555603", 0.3333333333333333}, /* :-/ */
{"1901", 0.3333333333333333}, /* Kippa */
{"15", 0.3333333333333333}, /* JKanStyle */
{"555555605", 0.3333333333333333}, /* :-\\ */
{"555555701", 0.3333333333333333}, /* ;-) */
{"487", 0.3333333333333333}, /* <] */
{"22639", 0.3333333333333333}, /* BabyRage */
{"555555572", 0.3333333333333333}, /* O.O */
{"65", 0.3333333333333333}, /* FrankerZ */
{"25", 0.3333333333333333}, /* Kappa */
{"36", 0.3333333333333333}, /* PJSalt */
{"499", 0.3333333333333333}, /* :) */
{"555555565", 0.3333333333333333}, /* :z */
{"434", 0.3333333333333333}, /* :( */
{"555555577", 0.3333333333333333}, /* B) */
{"34", 0.3333333333333333}, /* SwiftRage */
{"555555575", 0.3333333333333333}, /* o_o */
{"92", 0.3333333333333333}, /* PMSTwin */
{"555555570", 0.3333333333333333}, /* O.o */
{"555555569", 0.3333333333333333}, /* O_o */
{"493", 0.3333333333333333}, /* :/ */
{"26", 0.3333333333333333}, /* JonCarnage */
{"66", 0.3333333333333333}, /* OneHand */
{"973", 0.3333333333333333}, /* DAESuppy */
{"555555568", 0.3333333333333333}, /* :-Z */
{"555555599", 0.3333333333333333}, /* R) */
{"1900", 0.3333333333333333}, /* RalpherZ */
{"555555582", 0.3333333333333333}, /* :o */
{"1899", 0.3333333333333333}, /* TF2John */
{"555555633", 0.3333333333333333}, /* ;P */
{"16", 0.3333333333333333}, /* OptimizePrime */
{"30", 0.3333333333333333}, /* BCWarrior */
{"555555583", 0.3333333333333333}, /* :-o */
{"32", 0.3333333333333333}, /* GingerPower */
{"87", 0.3333333333333333}, /* ShazBotstix */
{"74", 0.3333333333333333}, /* AsianGlow */
{"555555571", 0.3333333333333333}, /* O_O */
{"46", 0.3333333333333333}, /* SSSsss */
};
auto it = outliers.find(id.string);
if (it != outliers.end())
{
return it->second;
}
return default3xScaleFactor;
}
} // namespace
namespace chatterino {
QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode)
@ -44,14 +438,15 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
if (!shared)
{
// From Twitch docs - expected size for an emote (1x)
constexpr QSize baseSize(28, 28);
auto baseSize = getEmoteExpectedBaseSize(id);
auto emote3xScaleFactor = getEmote3xScaleFactor(id);
(*cache)[id] = shared = std::make_shared<Emote>(Emote{
EmoteName{name},
ImageSet{
Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize),
Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2),
Image::fromUrl(getEmoteLink(id, "3.0"), 0.25, baseSize * 4),
Image::fromUrl(getEmoteLink(id, "3.0"), emote3xScaleFactor,
baseSize * (1.0 / emote3xScaleFactor)),
},
Tooltip{name.toHtmlEscaped() + "<br>Twitch Emote"},
});
@ -60,11 +455,4 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id,
return shared;
}
Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale)
{
return {QString(TWITCH_EMOTE_TEMPLATE)
.replace("{id}", id.string)
.replace("{scale}", emoteScale)};
}
} // namespace chatterino

View file

@ -52,7 +52,6 @@ public:
const EmoteName &name) override;
private:
Url getEmoteLink(const EmoteId &id, const QString &emoteScale);
UniqueAccess<std::unordered_map<EmoteId, std::weak_ptr<Emote>>>
twitchEmotesCache_;
};

View file

@ -39,9 +39,17 @@ const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
void sendHelixMessage(const std::shared_ptr<TwitchChannel> &channel,
const QString &message, const QString &replyParentId = {})
{
auto broadcasterID = channel->roomId();
if (broadcasterID.isEmpty())
{
channel->addMessage(makeSystemMessage(
"Sending messages in this channel isn't possible."));
return;
}
getHelix()->sendChatMessage(
{
.broadcasterID = channel->roomId(),
.broadcasterID = broadcasterID,
.senderID =
getIApp()->getAccounts()->twitch.getCurrent()->getUserId(),
.message = message,
@ -68,13 +76,18 @@ void sendHelixMessage(const std::shared_ptr<TwitchChannel> &channel,
}();
chan->addMessage(errorMessage);
},
[weak = std::weak_ptr(channel)](auto error, const auto &message) {
[weak = std::weak_ptr(channel)](auto error, auto message) {
auto chan = weak.lock();
if (!chan)
{
return;
}
if (message.isEmpty())
{
message = "(empty message)";
}
using Error = decltype(error);
auto errorMessage = [&]() -> QString {

View file

@ -51,6 +51,8 @@ using namespace chatterino::literals;
namespace {
const QColor AUTOMOD_USER_COLOR{"blue"};
using namespace std::chrono_literals;
const QString regexHelpString("(\\w+)[.,!?;:]*?$");
@ -756,7 +758,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
QString username = match.captured(1);
auto originalTextColor = textColor;
if (this->twitchChannel != nullptr && getSettings()->colorUsernames)
if (this->twitchChannel != nullptr)
{
if (auto userColor =
this->twitchChannel->getUserColor(username);
@ -767,21 +769,17 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
}
auto prefixedUsername = '@' + username;
this->emplace<TextElement>(prefixedUsername,
MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold)
auto remainder = string.remove(prefixedUsername);
this->emplace<MentionElement>(prefixedUsername, originalTextColor,
textColor)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
->setTrailingSpace(remainder.isEmpty());
this->emplace<TextElement>(prefixedUsername,
MessageElementFlag::NonBoldUsername,
textColor)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
this->emplace<TextElement>(string.remove(prefixedUsername),
MessageElementFlag::Text,
originalTextColor);
if (!remainder.isEmpty())
{
this->emplace<TextElement>(remainder, MessageElementFlag::Text,
originalTextColor);
}
return;
}
@ -797,30 +795,23 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
{
auto originalTextColor = textColor;
if (getSettings()->colorUsernames)
if (auto userColor = this->twitchChannel->getUserColor(username);
userColor.isValid())
{
if (auto userColor =
this->twitchChannel->getUserColor(username);
userColor.isValid())
{
textColor = userColor;
}
textColor = userColor;
}
this->emplace<TextElement>(username,
MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold)
auto remainder = string.remove(username);
this->emplace<MentionElement>(username, originalTextColor,
textColor)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
->setTrailingSpace(remainder.isEmpty());
this->emplace<TextElement>(
username, MessageElementFlag::NonBoldUsername, textColor)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
this->emplace<TextElement>(string.remove(username),
MessageElementFlag::Text,
originalTextColor);
if (!remainder.isEmpty())
{
this->emplace<TextElement>(remainder, MessageElementFlag::Text,
originalTextColor);
}
return;
}
@ -893,8 +884,10 @@ void TwitchMessageBuilder::parseThread()
->setLink({Link::ViewThread, this->thread_->rootId()});
this->emplace<TextElement>(
"@" + usernameText + ":", MessageElementFlag::RepliedMessage,
threadRoot->usernameColor, FontStyle::ChatMediumSmall)
"@" + usernameText +
(threadRoot->flags.has(MessageFlag::Action) ? "" : ":"),
MessageElementFlag::RepliedMessage, threadRoot->usernameColor,
FontStyle::ChatMediumSmall)
->setLink({Link::UserInfo, threadRoot->displayName});
MessageColor color = MessageColor::Text;
@ -1623,6 +1616,8 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage(
builder->message().messageText = textList.join(" ");
builder->message().searchText = textList.join(" ");
builder->message().loginName = reward.user.login;
builder->message().reward = std::make_shared<ChannelPointReward>(reward);
}
void TwitchMessageBuilder::liveMessage(const QString &channelName,
@ -1817,7 +1812,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix,
MessageColor color = MessageColor::System;
if (tc && getSettings()->colorUsernames)
if (tc)
{
if (auto userColor = tc->getUserColor(username);
userColor.isValid())
@ -1826,14 +1821,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(QString prefix,
}
}
builder
->emplace<TextElement>(username, MessageElementFlag::BoldUsername,
color, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
builder
->emplace<TextElement>(username,
MessageElementFlag::NonBoldUsername, color)
builder->emplace<MentionElement>(username, MessageColor::System, color)
->setLink({Link::UserInfo, username})
->setTrailingSpace(false);
}
@ -1869,7 +1857,7 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(
MessageColor color = MessageColor::System;
if (tc && getSettings()->colorUsernames)
if (tc)
{
if (auto userColor = tc->getUserColor(user.userLogin);
userColor.isValid())
@ -1879,14 +1867,8 @@ void TwitchMessageBuilder::listOfUsersSystemMessage(
}
builder
->emplace<TextElement>(user.userName,
MessageElementFlag::BoldUsername, color,
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, user.userLogin})
->setTrailingSpace(false);
builder
->emplace<TextElement>(user.userName,
MessageElementFlag::NonBoldUsername, color)
->emplace<MentionElement>(user.userName, MessageColor::System,
color)
->setLink({Link::UserInfo, user.userLogin})
->setTrailingSpace(false);
}
@ -1956,12 +1938,8 @@ MessagePtr TwitchMessageBuilder::makeAutomodInfoMessage(
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
MessageColor(QColor("blue")),
FontStyle::ChatMediumBold);
builder.emplace<TextElement>(
"AutoMod:", MessageElementFlag::NonBoldUsername,
MessageColor(QColor("blue")));
builder.emplace<MentionElement>("AutoMod:", AUTOMOD_USER_COLOR,
AUTOMOD_USER_COLOR);
switch (action.type)
{
case AutomodInfoAction::OnHold: {
@ -2015,12 +1993,8 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeAutomodMessage(
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
MessageColor(QColor("blue")),
FontStyle::ChatMediumBold);
builder.emplace<TextElement>(
"AutoMod:", MessageElementFlag::NonBoldUsername,
MessageColor(QColor("blue")));
builder2.emplace<MentionElement>("AutoMod:", AUTOMOD_USER_COLOR,
AUTOMOD_USER_COLOR);
// AutoMod header message
builder.emplace<TextElement>(
("Held a message for reason: " + action.reason +
@ -2068,14 +2042,8 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeAutomodMessage(
// sender username
builder2
.emplace<TextElement>(
action.target.displayName + ":", MessageElementFlag::BoldUsername,
MessageColor(action.target.color), FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.target.login});
builder2
.emplace<TextElement>(action.target.displayName + ":",
MessageElementFlag::NonBoldUsername,
MessageColor(action.target.color))
.emplace<MentionElement>(action.target.displayName + ":",
MessageColor::Text, action.target.color)
->setLink({Link::UserInfo, action.target.login});
// sender's message caught by AutoMod
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
@ -2271,17 +2239,9 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
appendBadges(&builder2, action.senderBadges, {}, twitchChannel);
// sender username
builder2
.emplace<TextElement>(action.suspiciousUserDisplayName + ":",
MessageElementFlag::BoldUsername,
MessageColor(action.suspiciousUserColor),
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder2
.emplace<TextElement>(action.suspiciousUserDisplayName + ":",
MessageElementFlag::NonBoldUsername,
MessageColor(action.suspiciousUserColor))
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder2.emplace<MentionElement>(action.suspiciousUserDisplayName + ":",
MessageColor::Text,
action.suspiciousUserColor);
// sender's message caught by AutoMod
for (const auto &fragment : action.fragments)

View file

@ -5,6 +5,7 @@
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "util/CancellationToken.hpp"
#include "util/QMagicEnum.hpp"
#include <magic_enum/magic_enum.hpp>
#include <QJsonDocument>
@ -1172,9 +1173,7 @@ void Helix::sendChatAnnouncement(
QJsonObject body;
body.insert("message", message);
const auto colorStr =
std::string{magic_enum::enum_name<HelixAnnouncementColor>(color)};
body.insert("color", QString::fromStdString(colorStr).toLower());
body.insert("color", qmagicenum::enumNameString(color).toLower());
this->makePost("chat/announcements", urlQuery)
.json(body)

View file

@ -1,5 +1,7 @@
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root)
@ -7,7 +9,7 @@ PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root)
, data(root.value("data").toObject())
, status(this->data.value("status").toString())
{
auto oType = magic_enum::enum_cast<Type>(this->typeString.toStdString());
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();

View file

@ -1,5 +1,7 @@
#include "providers/twitch/pubsubmessages/Base.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubMessage::PubSubMessage(QJsonObject _object)
@ -9,7 +11,7 @@ PubSubMessage::PubSubMessage(QJsonObject _object)
, error(this->object.value("error").toString())
, typeString(this->object.value("type").toString())
{
auto oType = magic_enum::enum_cast<Type>(this->typeString.toStdString());
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();

View file

@ -1,5 +1,7 @@
#include "providers/twitch/pubsubmessages/ChannelPoints.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message(
@ -7,7 +9,7 @@ PubSubCommunityPointsChannelV1Message::PubSubCommunityPointsChannelV1Message(
: typeString(root.value("type").toString())
, data(root.value("data").toObject())
{
auto oType = magic_enum::enum_cast<Type>(this->typeString.toStdString());
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();

View file

@ -1,5 +1,7 @@
#include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage(
@ -7,7 +9,7 @@ PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage(
: typeString(root.value("type").toString())
, data(root.value("data").toObject())
{
auto oType = magic_enum::enum_cast<Type>(this->typeString.toStdString());
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();

View file

@ -1,5 +1,7 @@
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include "util/QMagicEnum.hpp"
#include <QDateTime>
#include <QJsonArray>
@ -8,8 +10,7 @@ namespace chatterino {
PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
: typeString(root.value("type").toString())
{
if (const auto oType =
magic_enum::enum_cast<Type>(this->typeString.toStdString());
if (const auto oType = qmagicenum::enumCast<Type>(this->typeString);
oType.has_value())
{
this->type = oType.value();
@ -75,8 +76,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
this->updatedByUserDisplayName = updatedBy.value("display_name").toString();
this->treatmentString = data.value("treatment").toString();
if (const auto oTreatment = magic_enum::enum_cast<Treatment>(
this->treatmentString.toStdString());
if (const auto oTreatment =
qmagicenum::enumCast<Treatment>(this->treatmentString);
oTreatment.has_value())
{
this->treatment = oTreatment.value();
@ -84,8 +85,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
this->evasionEvaluationString =
data.value("ban_evasion_evaluation").toString();
if (const auto oEvaluation = magic_enum::enum_cast<EvasionEvaluation>(
this->evasionEvaluationString.toStdString());
if (const auto oEvaluation = qmagicenum::enumCast<EvasionEvaluation>(
this->evasionEvaluationString);
oEvaluation.has_value())
{
this->evasionEvaluation = oEvaluation.value();
@ -93,8 +94,8 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
for (const auto &rType : data.value("types").toArray())
{
if (const auto oRestriction = magic_enum::enum_cast<RestrictionType>(
rType.toString().toStdString());
if (const auto oRestriction =
qmagicenum::enumCast<RestrictionType>(rType.toString());
oRestriction.has_value())
{
this->restrictionTypes.set(oRestriction.value());

View file

@ -1,11 +1,13 @@
#include "providers/twitch/pubsubmessages/Whisper.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubWhisperMessage::PubSubWhisperMessage(const QJsonObject &root)
: typeString(root.value("type").toString())
{
auto oType = magic_enum::enum_cast<Type>(this->typeString.toStdString());
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();

View file

@ -51,8 +51,11 @@ const QStringList &broadcastingBinaries()
bool isBroadcasterSoftwareActive()
{
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
static bool shouldShowTimeoutWarning = true;
static bool shouldShowWarning = true;
QProcess p;
p.start("pgrep", {"-x", broadcastingBinaries().join("|")},
p.start("pgrep", {"-xi", broadcastingBinaries().join("|")},
QIODevice::NotOpen);
if (p.waitForFinished(1000) && p.exitStatus() == QProcess::NormalExit)
@ -62,20 +65,46 @@ bool isBroadcasterSoftwareActive()
// Fallback to false and showing a warning
static bool shouldShowWarning = true;
if (shouldShowWarning)
switch (p.error())
{
shouldShowWarning = false;
case QProcess::Timedout: {
qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!";
if (shouldShowTimeoutWarning)
{
shouldShowTimeoutWarning = false;
postToThread([] {
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.");
});
postToThread([] {
getApp()->twitch->addGlobalSystemMessage(
"Streamer Mode is set to Automatic, but pgrep timed "
"out. This can happen if your system lagged at the "
"wrong moment. If Streamer Mode continues to not work, "
"you can manually set it to Enabled or Disabled in the "
"Settings.");
});
}
}
break;
default: {
qCWarning(chatterinoStreamerMode)
<< "pgrep execution failed:" << p.error();
if (shouldShowWarning)
{
shouldShowWarning = false;
postToThread([] {
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.");
});
}
}
break;
}
qCWarning(chatterinoStreamerMode) << "pgrep execution timed out!";
return false;
#elif defined(Q_OS_WIN)
if (!IsWindowsVistaOrGreater())

View file

@ -1,6 +1,7 @@
#include "Toasts.hpp"
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/Literals.hpp"
#include "common/QLogging.hpp"
#include "common/Version.hpp"
@ -177,9 +178,8 @@ public:
case ToastReaction::OpenInPlayer:
if (platform_ == Platform::Twitch)
{
QDesktopServices::openUrl(QUrl(
u"https://player.twitch.tv/?parent=twitch.tv&channel=" %
channelName_));
QDesktopServices::openUrl(
QUrl(TWITCH_PLAYER_URL.arg(channelName_)));
}
break;
case ToastReaction::OpenInStreamlink: {

View file

@ -108,7 +108,6 @@ WindowManager::WindowManager(const Paths &paths)
this->wordFlagsListener_.addSetting(settings->showBadgesFfz);
this->wordFlagsListener_.addSetting(settings->showBadgesSevenTV);
this->wordFlagsListener_.addSetting(settings->enableEmoteImages);
this->wordFlagsListener_.addSetting(settings->boldUsernames);
this->wordFlagsListener_.addSetting(settings->lowercaseDomains);
this->wordFlagsListener_.addSetting(settings->showReplyButton);
this->wordFlagsListener_.setCB([this] {
@ -182,8 +181,6 @@ void WindowManager::updateWordTypeMask()
// misc
flags.set(MEF::AlwaysShow);
flags.set(MEF::Collapsed);
flags.set(settings->boldUsernames ? MEF::BoldUsername
: MEF::NonBoldUsername);
flags.set(MEF::LowercaseLinks, settings->lowercaseDomains);
flags.set(MEF::ChannelPointReward);
@ -422,6 +419,13 @@ void WindowManager::initialize(Settings &settings, const Paths &paths)
this->forceLayoutChannelViews();
});
settings.colorUsernames.connect([this](auto, auto) {
this->forceLayoutChannelViews();
});
settings.boldUsernames.connect([this](auto, auto) {
this->forceLayoutChannelViews();
});
this->initialized_ = true;
}

View file

@ -5,6 +5,7 @@
#include "common/network/NetworkRequest.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "debug/Benchmark.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "singletons/imageuploader/UploadedImageModel.hpp"
@ -28,6 +29,8 @@
#include <memory>
#include <utility>
#define UPLOAD_DELAY 2000
// Delay between uploads in milliseconds
@ -281,6 +284,11 @@ void ImageUploader::handleFailedUpload(const NetworkResult &result,
}
channel->addMessage(makeSystemMessage(errorMessage));
// NOTE: We abort any future uploads on failure. Should this be handled differently?
while (!this->uploadQueue_.empty())
{
this->uploadQueue_.pop();
}
this->uploadMutex_.unlock();
}
@ -334,22 +342,20 @@ void ImageUploader::handleSuccessfulUpload(const NetworkResult &result,
this->logToFile(originalFilePath, link, deletionLink, channel);
}
void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
QPointer<ResizingTextEdit> outputTextEdit)
std::pair<std::queue<RawImageData>, QString> ImageUploader::getImages(
const QMimeData *source) const
{
if (!this->uploadMutex_.tryLock())
{
channel->addMessage(makeSystemMessage(
QString("Please wait until the upload finishes.")));
return;
}
BenchmarkGuard benchmarkGuard("ImageUploader::getImages");
channel->addMessage(makeSystemMessage(QString("Started upload...")));
auto tryUploadFromUrls = [&]() -> bool {
auto tryUploadFromUrls =
[&]() -> std::pair<std::queue<RawImageData>, QString> {
if (!source->hasUrls())
{
return false;
return {{}, {}};
}
std::queue<RawImageData> images;
auto mimeDb = QMimeDatabase();
// This path gets chosen when files are copied from a file manager, like explorer.exe, caja.
// Each entry in source->urls() is a QUrl pointing to a file that was copied.
@ -359,101 +365,118 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
QMimeType mime = mimeDb.mimeTypeForUrl(path);
if (mime.name().startsWith("image") && !mime.inherits("image/gif"))
{
channel->addMessage(makeSystemMessage(
QString("Uploading image: %1").arg(localPath)));
QImage img = QImage(localPath);
if (img.isNull())
{
channel->addMessage(
makeSystemMessage(QString("Couldn't load image :(")));
return false;
return {{}, "Couldn't load image :("};
}
auto imageData = convertToPng(img);
if (imageData)
if (!imageData)
{
RawImageData data = {*imageData, "png", localPath};
this->uploadQueue_.push(data);
}
else
{
channel->addMessage(makeSystemMessage(
return {
{},
QString("Cannot upload file: %1. Couldn't convert "
"image to png.")
.arg(localPath)));
return false;
.arg(localPath),
};
}
images.push({*imageData, "png", localPath});
}
else if (mime.inherits("image/gif"))
{
channel->addMessage(makeSystemMessage(
QString("Uploading GIF: %1").arg(localPath)));
QFile file(localPath);
bool isOkay = file.open(QIODevice::ReadOnly);
if (!isOkay)
{
channel->addMessage(
makeSystemMessage(QString("Failed to open file. :(")));
return false;
return {{}, "Failed to open file :("};
}
// file.readAll() => might be a bit big but it /should/ work
RawImageData data = {file.readAll(), "gif", localPath};
this->uploadQueue_.push(data);
images.push({file.readAll(), "gif", localPath});
file.close();
}
}
if (!this->uploadQueue_.empty())
{
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
outputTextEdit);
this->uploadQueue_.pop();
return true;
}
return false;
return {images, {}};
};
auto tryUploadDirectly = [&]() -> bool {
auto tryUploadDirectly =
[&]() -> std::pair<std::queue<RawImageData>, QString> {
std::queue<RawImageData> images;
if (source->hasFormat("image/png"))
{
// the path to file is not present every time, thus the filePath is empty
this->sendImageUploadRequest({source->data("image/png"), "png", ""},
channel, outputTextEdit);
return true;
images.push({source->data("image/png"), "png", ""});
return {images, {}};
}
if (source->hasFormat("image/jpeg"))
{
this->sendImageUploadRequest(
{source->data("image/jpeg"), "jpeg", ""}, channel,
outputTextEdit);
return true;
images.push({source->data("image/jpeg"), "jpeg", ""});
return {images, {}};
}
if (source->hasFormat("image/gif"))
{
this->sendImageUploadRequest({source->data("image/gif"), "gif", ""},
channel, outputTextEdit);
return true;
images.push({source->data("image/gif"), "gif", ""});
return {images, {}};
}
// not PNG, try loading it into QImage and save it to a PNG.
auto image = qvariant_cast<QImage>(source->imageData());
auto imageData = convertToPng(image);
if (imageData)
{
sendImageUploadRequest({*imageData, "png", ""}, channel,
outputTextEdit);
return true;
images.push({*imageData, "png", ""});
return {images, {}};
}
// No direct upload happenned
channel->addMessage(makeSystemMessage(
QString("Cannot upload file, failed to convert to png.")));
return false;
return {{}, "Cannot upload file, failed to convert to png."};
};
if (!tryUploadFromUrls() && !tryUploadDirectly())
const auto [urlImageData, urlError] = tryUploadFromUrls();
if (!urlImageData.empty())
{
channel->addMessage(
makeSystemMessage(QString("Cannot upload file from clipboard.")));
this->uploadMutex_.unlock();
return {urlImageData, {}};
}
const auto [directImageData, directError] = tryUploadDirectly();
if (!directImageData.empty())
{
return {directImageData, {}};
}
return {
{},
// TODO: verify that this looks ok xd
urlError + directError,
};
}
void ImageUploader::upload(std::queue<RawImageData> images, ChannelPtr channel,
QPointer<ResizingTextEdit> outputTextEdit)
{
BenchmarkGuard benchmarkGuard("upload");
if (!this->uploadMutex_.tryLock())
{
channel->addMessage(makeSystemMessage(
QString("Please wait until the upload finishes.")));
return;
}
assert(!images.empty());
assert(this->uploadQueue_.empty());
std::swap(this->uploadQueue_, images);
channel->addMessage(makeSystemMessage("Started upload..."));
this->sendImageUploadRequest(this->uploadQueue_.front(), std::move(channel),
std::move(outputTextEdit));
this->uploadQueue_.pop();
}
} // namespace chatterino

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