mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Merge remote-tracking branch 'origin/master' into chore/eventsub
This commit is contained in:
commit
5616dc48b3
|
@ -21,15 +21,12 @@ deb_path="Chatterino-ubuntu-${ubuntu_release}-x86_64.deb"
|
|||
# Refactor opportunity:
|
||||
case "$ubuntu_release" in
|
||||
20.04)
|
||||
dependencies="libc6, libstdc++6, libqt5core5a, libqt5concurrent5, libqt5dbus5, libqt5gui5, libqt5network5, libqt5svg5, libqt5widgets5, qt5-image-formats-plugins, libboost-filesystem1.71.0"
|
||||
# Qt6 static-linked deb, see https://github.com/Chatterino/docker
|
||||
dependencies="libc6, libstdc++6, libblkid1, libbsd0, libc6, libexpat1, libffi7, libfontconfig1, libfreetype6, libglib2.0-0, libglvnd0, libglx0, libgraphite2-3, libharfbuzz0b, libicu66, libjpeg-turbo8, libmount1, libopengl0, libpcre2-16-0, libpcre3, libpng16-16, libselinux1, libssl1.1, libstdc++6, libuuid1, libx11-xcb1, libxau6, libxcb1, libxcb-cursor0, libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render0, libxcb-render-util0, libxcb-shape0, libxcb-shm0, libxcb-sync1, libxcb-util1, libxcb-xfixes0, libxcb-xkb1, libxdmcp6, libxkbcommon0, libxkbcommon-x11-0, zlib1g"
|
||||
;;
|
||||
22.04)
|
||||
if [ -n "$Qt6_DIR" ]; then
|
||||
echo "Qt6_DIR set, assuming Qt6"
|
||||
dependencies="libc6, libstdc++6, libqt6core6, libqt6widgets6, libqt6network6, libqt6core5compat6, libqt6svg6, qt6-qpa-plugins, qt6-image-formats-plugins"
|
||||
else
|
||||
dependencies="libc6, libstdc++6, libqt5core5a, libqt5concurrent5, libqt5dbus5, libqt5gui5, libqt5network5, libqt5svg5, libqt5widgets5, qt5-image-formats-plugins, libboost-filesystem1.74.0"
|
||||
fi
|
||||
# Qt6 static-linked deb, see https://github.com/Chatterino/docker
|
||||
dependencies="libc6, libstdc++6, libglx0, libopengl0, libpng16-16, libharfbuzz0b, libfreetype6, libfontconfig1, libjpeg-turbo8, libxcb-glx0, libegl1, libx11-6, libxkbcommon0, libx11-xcb1, libxkbcommon-x11-0, libxcb-cursor0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, libxcb-render-util0, libxcb-shm0, libxcb-sync1, libxcb-xfixes0, libxcb-render0, libxcb-shape0, libxcb-xkb1, libxcb1, libbrotli1, libglib2.0-0, zlib1g, libicu70, libpcre2-16-0, libssl3, libgraphite2-3, libexpat1, libuuid1, libxcb-util1, libxau6, libxdmcp6, libbrotli1, libffi8, libmount1, libselinux1, libpcre3, libicu70, libbsd0, libblkid1, libpcre2-8-0, libmd0"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported Ubuntu release $ubuntu_release"
|
||||
|
|
30
.CI/full-ubuntu-build.sh
Executable file
30
.CI/full-ubuntu-build.sh
Executable file
|
@ -0,0 +1,30 @@
|
|||
#!/bin/sh
|
||||
|
||||
# TODO: Investigate if the -fno-sized-deallocation flag is still necessary
|
||||
# TODO: Test appimage/deb creation
|
||||
|
||||
set -e
|
||||
|
||||
env
|
||||
|
||||
rm -rf build
|
||||
mkdir build
|
||||
cmake \
|
||||
-B build \
|
||||
-DCMAKE_INSTALL_PREFIX=appdir/usr/ \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_APP=On \
|
||||
-DBUILD_TESTS=On \
|
||||
-DBUILD_BENCHMARKS=On \
|
||||
-DUSE_PRECOMPILED_HEADERS=OFF \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=On \
|
||||
-DCHATTERINO_PLUGINS="$C2_PLUGINS" \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \
|
||||
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
|
||||
-DCHATTERINO_STATIC_QT_BUILD=On \
|
||||
-DCMAKE_CXX_FLAGS="-fno-sized-deallocation" \
|
||||
.
|
||||
cmake --build build
|
||||
|
||||
# sh ./../.CI/CreateAppImage.sh
|
||||
# sh ./../.CI/CreateUbuntuDeb.sh
|
254
.github/workflows/build.yml
vendored
254
.github/workflows/build.yml
vendored
|
@ -24,33 +24,93 @@ env:
|
|||
CONAN_VERSION: 2.0.2
|
||||
|
||||
jobs:
|
||||
build-ubuntu-docker:
|
||||
name: "Build Ubuntu in Docker"
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.container }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
container: ghcr.io/chatterino/chatterino2-build-ubuntu-20.04:latest
|
||||
qt-version: 6.6.1
|
||||
force-lto: false
|
||||
plugins: true
|
||||
skip-artifact: false
|
||||
skip-crashpad: false
|
||||
build-appimage: false
|
||||
build-deb: true
|
||||
- os: ubuntu-22.04
|
||||
container: ghcr.io/chatterino/chatterino2-build-ubuntu-22.04:latest
|
||||
qt-version: 6.6.1
|
||||
force-lto: false
|
||||
plugins: true
|
||||
skip-artifact: false
|
||||
skip-crashpad: false
|
||||
build-appimage: true
|
||||
build-deb: true
|
||||
env:
|
||||
C2_ENABLE_LTO: ${{ matrix.force-lto }}
|
||||
C2_PLUGINS: ${{ matrix.plugins }}
|
||||
C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }}
|
||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- 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" \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \
|
||||
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
|
||||
-DCHATTERINO_STATIC_QT_BUILD=On \
|
||||
..
|
||||
make -j"$(nproc)"
|
||||
|
||||
- name: Package - AppImage (Ubuntu)
|
||||
if: matrix.build-appimage
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateAppImage.sh
|
||||
|
||||
- name: Upload artifact - AppImage (Ubuntu)
|
||||
if: matrix.build-appimage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-x86_64-Qt-${{ matrix.qt-version }}.AppImage
|
||||
path: build/Chatterino-x86_64.AppImage
|
||||
|
||||
- name: Package - .deb (Ubuntu)
|
||||
if: matrix.build-deb
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateUbuntuDeb.sh
|
||||
|
||||
- name: Upload artifact - .deb (Ubuntu)
|
||||
if: matrix.build-deb
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb
|
||||
path: build/Chatterino-${{ matrix.os }}-x86_64.deb
|
||||
|
||||
build:
|
||||
name: "Build ${{ matrix.os }}, Qt ${{ matrix.qt-version }} (LTO:${{ matrix.force-lto }}, crashpad:${{ matrix.skip-crashpad && 'off' || 'on' }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# Ubuntu 20.04, Qt 5.12
|
||||
- os: ubuntu-20.04
|
||||
qt-version: 5.12.12
|
||||
force-lto: false
|
||||
plugins: false
|
||||
skip-artifact: false
|
||||
skip-crashpad: false
|
||||
# Ubuntu 22.04, Qt 5.15
|
||||
- os: ubuntu-22.04
|
||||
qt-version: 5.15.2
|
||||
force-lto: false
|
||||
plugins: false
|
||||
skip-artifact: false
|
||||
skip-crashpad: false
|
||||
# Ubuntu 22.04, Qt 6.2.4 - tests LTO & plugins
|
||||
- os: ubuntu-22.04
|
||||
qt-version: 6.2.4
|
||||
force-lto: true
|
||||
plugins: true
|
||||
skip-artifact: false
|
||||
skip-crashpad: false
|
||||
# macOS
|
||||
- os: macos-latest
|
||||
qt-version: 5.15.2
|
||||
|
@ -74,38 +134,13 @@ jobs:
|
|||
skip-crashpad: true
|
||||
|
||||
fail-fast: false
|
||||
env:
|
||||
C2_ENABLE_LTO: ${{ matrix.force-lto }}
|
||||
C2_PLUGINS: ${{ matrix.plugins }}
|
||||
C2_ENABLE_CRASHPAD: ${{ matrix.skip-crashpad == false }}
|
||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }}
|
||||
|
||||
steps:
|
||||
- name: Force LTO
|
||||
if: matrix.force-lto
|
||||
run: |
|
||||
echo "C2_ENABLE_LTO=ON" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Enable plugin support
|
||||
if: matrix.plugins
|
||||
run: |
|
||||
echo "C2_PLUGINS=ON" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Set Crashpad
|
||||
if: matrix.skip-crashpad == false
|
||||
run: |
|
||||
echo "C2_ENABLE_CRASHPAD=ON" >> "$GITHUB_ENV"
|
||||
shell: bash
|
||||
|
||||
- name: Set environment variables for windows-latest
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
echo "vs_version=2022" >> "$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
|
||||
|
@ -274,83 +309,6 @@ jobs:
|
|||
run: conan cache clean --source --build --download "*"
|
||||
shell: bash
|
||||
|
||||
# LINUX
|
||||
- name: Install dependencies (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
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 Qt patches (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.qt-version, '5.')
|
||||
run: |
|
||||
patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch
|
||||
shell: bash
|
||||
|
||||
- name: Build (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
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" \
|
||||
..
|
||||
make -j"$(nproc)"
|
||||
shell: bash
|
||||
|
||||
- name: Package - AppImage (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateAppImage.sh
|
||||
shell: bash
|
||||
|
||||
- name: Package - .deb (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu') && !matrix.skip-artifact
|
||||
run: |
|
||||
cd build
|
||||
sh ./../.CI/CreateUbuntuDeb.sh
|
||||
shell: bash
|
||||
|
||||
- name: Upload artifact - AppImage (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu-20.04') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-x86_64-${{ matrix.qt-version }}.AppImage
|
||||
path: build/Chatterino-x86_64.AppImage
|
||||
|
||||
- name: Upload artifact - .deb (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu') && !matrix.skip-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Chatterino-${{ matrix.os }}-Qt-${{ matrix.qt-version }}.deb
|
||||
path: build/Chatterino-${{ matrix.os }}-x86_64.deb
|
||||
|
||||
# MACOS
|
||||
- name: Install dependencies (MacOS)
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
|
@ -394,8 +352,9 @@ jobs:
|
|||
with:
|
||||
name: chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||
path: build/chatterino-macos-Qt-${{ matrix.qt-version }}.dmg
|
||||
|
||||
create-release:
|
||||
needs: build
|
||||
needs: [build-ubuntu-docker, build]
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/master')
|
||||
|
||||
|
@ -404,12 +363,7 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0 # allows for tags access
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 22.04 Qt6.2.4 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-22.04-Qt-6.2.4.deb
|
||||
path: release-artifacts/
|
||||
|
||||
# Windows
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Windows Qt6.5.0
|
||||
with:
|
||||
|
@ -428,28 +382,23 @@ jobs:
|
|||
name: chatterino-windows-x86-64-Qt-5.15.2.zip
|
||||
path: release-artifacts/
|
||||
|
||||
# Linux
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Linux Qt5.12.12 AppImage
|
||||
name: Linux AppImage
|
||||
with:
|
||||
name: Chatterino-x86_64-5.12.12.AppImage
|
||||
name: Chatterino-x86_64-Qt-6.6.1.AppImage
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 20.04 Qt5.12.12 deb
|
||||
name: Ubuntu 20.04 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-20.04-Qt-5.12.12.deb
|
||||
name: Chatterino-ubuntu-20.04-Qt-6.6.1.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
name: Ubuntu 22.04 Qt5.15.2 deb
|
||||
name: Ubuntu 22.04 deb
|
||||
with:
|
||||
name: Chatterino-ubuntu-22.04-Qt-5.15.2.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
name: macOS x86_64 Qt5.15.2 dmg
|
||||
with:
|
||||
name: chatterino-macos-Qt-5.15.2.dmg
|
||||
name: Chatterino-ubuntu-22.04-Qt-6.6.1.deb
|
||||
path: release-artifacts/
|
||||
|
||||
- name: Copy flatpakref
|
||||
|
@ -457,21 +406,26 @@ jobs:
|
|||
cp .CI/chatterino-nightly.flatpakref release-artifacts/
|
||||
shell: bash
|
||||
|
||||
# macOS
|
||||
- uses: actions/download-artifact@v4
|
||||
name: macOS x86_64 Qt5.15.2 dmg
|
||||
with:
|
||||
name: chatterino-macos-Qt-5.15.2.dmg
|
||||
path: release-artifacts/
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
ls -l
|
||||
# Rename the macos build to indicate that it's for macOS 10.15 users
|
||||
mv chatterino-macos-Qt-5.15.2.dmg Chatterino-macOS-10.15.dmg
|
||||
|
||||
mv Chatterino-ubuntu-22.04-x86_64.deb EXPERIMENTAL-Chatterino-ubuntu-22.04-Qt-6.2.4.deb
|
||||
|
||||
# Mark all Windows Qt5 builds as old
|
||||
mv chatterino-windows-x86-64-Qt-5.15.2.zip chatterino-windows-old-x86-64-Qt-5.15.2.zip
|
||||
working-directory: release-artifacts
|
||||
shell: bash
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1.13.0
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
replacesArtifacts: true
|
||||
allowUpdates: true
|
||||
|
|
4
.github/workflows/clang-tidy.yml
vendored
4
.github/workflows/clang-tidy.yml
vendored
|
@ -119,7 +119,7 @@ jobs:
|
|||
|
||||
- name: clang-tidy review
|
||||
timeout-minutes: 20
|
||||
uses: ZedThree/clang-tidy-review@v0.17.0
|
||||
uses: ZedThree/clang-tidy-review@v0.17.1
|
||||
with:
|
||||
build_dir: build-clang-tidy
|
||||
config_file: ".clang-tidy"
|
||||
|
@ -145,4 +145,4 @@ jobs:
|
|||
libbenchmark-dev
|
||||
|
||||
- name: clang-tidy-review upload
|
||||
uses: ZedThree/clang-tidy-review/upload@v0.17.0
|
||||
uses: ZedThree/clang-tidy-review/upload@v0.17.1
|
||||
|
|
2
.github/workflows/post-clang-tidy-review.yml
vendored
2
.github/workflows/post-clang-tidy-review.yml
vendored
|
@ -14,6 +14,6 @@ jobs:
|
|||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
steps:
|
||||
- uses: ZedThree/clang-tidy-review/post@v0.17.0
|
||||
- uses: ZedThree/clang-tidy-review/post@v0.17.1
|
||||
with:
|
||||
lgtm_comment_body: ""
|
||||
|
|
102
.github/workflows/test.yml
vendored
102
.github/workflows/test.yml
vendored
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: Test
|
||||
name: Test Ubuntu
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -7,7 +7,7 @@ on:
|
|||
merge_group:
|
||||
|
||||
env:
|
||||
TWITCH_PUBSUB_SERVER_IMAGE: ghcr.io/chatterino/twitch-pubsub-server-test:v1.0.6
|
||||
TWITCH_PUBSUB_SERVER_TAG: v1.0.7
|
||||
QT_QPA_PLATFORM: minimal
|
||||
|
||||
concurrency:
|
||||
|
@ -16,91 +16,71 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: "${{ matrix.os }}"
|
||||
runs-on: ubuntu-latest
|
||||
container: ${{ matrix.container }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: "ubuntu-22.04"
|
||||
qt-version: "5.15.2"
|
||||
- os: "ubuntu-22.04"
|
||||
qt-version: "5.12.12"
|
||||
- os: "ubuntu-22.04"
|
||||
qt-version: "6.2.4"
|
||||
container: ghcr.io/chatterino/chatterino2-build-ubuntu-22.04:latest
|
||||
qt-version: 6.6.1
|
||||
plugins: true
|
||||
fail-fast: false
|
||||
env:
|
||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') && 'ON' || 'OFF' }}
|
||||
QT_MODULES: ${{ startsWith(matrix.qt-version, '6.') && 'qt5compat qtimageformats' || '' }}
|
||||
C2_PLUGINS: ${{ matrix.plugins }}
|
||||
C2_BUILD_WITH_QT6: ${{ startsWith(matrix.qt-version, '6.') }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3.3.0
|
||||
with:
|
||||
cache: true
|
||||
cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2
|
||||
modules: ${{ env.QT_MODULES }}
|
||||
version: ${{ matrix.qt-version }}
|
||||
|
||||
- name: Apply Qt patches (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.qt-version, '5.')
|
||||
run: |
|
||||
patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch
|
||||
shell: bash
|
||||
|
||||
# LINUX
|
||||
- name: Install dependencies (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
libbenchmark-dev \
|
||||
cmake \
|
||||
rapidjson-dev \
|
||||
libssl-dev \
|
||||
libboost-dev \
|
||||
libboost-system-dev \
|
||||
libboost-filesystem-dev \
|
||||
libpulse-dev \
|
||||
libxkbcommon-x11-0 \
|
||||
libgstreamer-plugins-base1.0-0 \
|
||||
build-essential \
|
||||
libgl1-mesa-dev \
|
||||
libxcb-icccm4 \
|
||||
libxcb-image0 \
|
||||
libxcb-keysyms1 \
|
||||
libxcb-render-util0 \
|
||||
libxcb-xinerama0
|
||||
|
||||
- name: Create build directory (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: mkdir build-test
|
||||
|
||||
- name: Install googlebench
|
||||
run: |
|
||||
mkdir build-test
|
||||
shell: bash
|
||||
sudo apt update
|
||||
sudo apt -y install \
|
||||
libbenchmark-dev
|
||||
|
||||
- name: Build (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
cmake \
|
||||
-DBUILD_TESTS=On \
|
||||
-DBUILD_BENCHMARKS=On \
|
||||
-DBUILD_APP=OFF \
|
||||
-DCHATTERINO_PLUGINS="$C2_PLUGINS" \
|
||||
-DCMAKE_PREFIX_PATH="$Qt6_DIR/lib/cmake" \
|
||||
-DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \
|
||||
-DCHATTERINO_STATIC_QT_BUILD=On \
|
||||
..
|
||||
cmake --build .
|
||||
working-directory: build-test
|
||||
shell: bash
|
||||
|
||||
- name: Test (Ubuntu)
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
- name: Download and extract Twitch PubSub Server Test
|
||||
run: |
|
||||
mkdir pubsub-server-test
|
||||
curl -L -o pubsub-server.tar.gz "https://github.com/Chatterino/twitch-pubsub-server-test/releases/download/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/server-${{ env.TWITCH_PUBSUB_SERVER_TAG }}-linux-amd64.tar.gz"
|
||||
tar -xzf pubsub-server.tar.gz -C pubsub-server-test
|
||||
rm pubsub-server.tar.gz
|
||||
cd pubsub-server-test
|
||||
curl -L -o server.crt "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.crt"
|
||||
curl -L -o server.key "https://github.com/Chatterino/twitch-pubsub-server-test/raw/${{ env.TWITCH_PUBSUB_SERVER_TAG }}/cmd/server/server.key"
|
||||
cd ..
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cargo Install httpbox
|
||||
run: |
|
||||
cargo install --git https://github.com/kevinastone/httpbox --rev 89b971f
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
docker pull kennethreitz/httpbin
|
||||
docker pull ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
||||
docker run --network=host --detach ${{ env.TWITCH_PUBSUB_SERVER_IMAGE }}
|
||||
docker run -p 9051:80 --detach kennethreitz/httpbin
|
||||
ctest --repeat until-pass:4 --output-on-failure
|
||||
httpbox --port 9051 &
|
||||
cd ../pubsub-server-test
|
||||
./server 127.0.0.1:9050 &
|
||||
cd ../build-test
|
||||
ctest --repeat until-pass:4 --output-on-failure --exclude-regex ClassicEmoteNameFiltering
|
||||
working-directory: build-test
|
||||
shell: bash
|
||||
|
|
|
@ -77,19 +77,6 @@ Note: This installation will take about 2.1 GB of disk space.
|
|||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>OpenSSL</summary>
|
||||
|
||||
For our websocket library, we need OpenSSL 1.1.
|
||||
|
||||
1. Download OpenSSL for windows, version `1.1.1s`: **[Download](https://web.archive.org/web/20221101204129/https://slproweb.com/download/Win64OpenSSL-1_1_1s.exe)**
|
||||
2. When prompted, install OpenSSL to `C:\local\openssl`
|
||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||
|
||||
Note: This installation will take about 200 MB of disk space.
|
||||
|
||||
</details>
|
||||
|
||||
## Building
|
||||
|
||||
### Using CMake
|
||||
|
|
42
CHANGELOG.md
42
CHANGELOG.md
|
@ -16,17 +16,29 @@
|
|||
- 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)
|
||||
- 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)
|
||||
- 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 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 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)
|
||||
- Minor: Updated to Emoji v15.1. Google emojis are now used as the fallback instead of Twitter emojis. (#5182)
|
||||
- Minor: Allow theming of tab live and rerun indicators. (#5188)
|
||||
- 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)
|
||||
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
||||
- 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)
|
||||
|
@ -41,26 +53,28 @@
|
|||
- 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 reply context didn't render correctly if an emoji was touching text. (#4875, #4977)
|
||||
- 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 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)
|
||||
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126)
|
||||
- Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110)
|
||||
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965)
|
||||
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
|
||||
- Bugfix: Fixed some emotes not appearing when using _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 support for Windows 11 Snap layouts. (#4994)
|
||||
- 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 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)
|
||||
|
@ -70,9 +84,15 @@
|
|||
- 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: 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)
|
||||
- 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)
|
||||
|
@ -96,6 +116,7 @@
|
|||
- Dev: Replace `boost::optional` with `std::optional`. (#4877)
|
||||
- Dev: Improve performance of selecting text. (#4889, #4911)
|
||||
- Dev: Removed direct dependency on Qt 5 compatibility module. (#4906)
|
||||
- Dev: Added unit test capabilities to SplitInput. (#5179)
|
||||
- Dev: Refactor `Emoji`'s EmojiMap into a vector. (#4980)
|
||||
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
|
||||
- Dev: Refactor `common/Credentials`. (#4979)
|
||||
|
@ -110,11 +131,14 @@
|
|||
- Dev: `Details` file properties tab is now populated on Windows. (#4912)
|
||||
- Dev: Removed `Outcome` from network requests. (#4959)
|
||||
- Dev: Added Tests for Windows and MacOS in CI. (#4970, #5032)
|
||||
- Dev: Added "Copy message as JSON" option when shift-right-clicking a message. (#5150)
|
||||
- Dev: Windows now builds with Qt6 by default. (#5155)
|
||||
- Dev: Conan now uses OpenSSL 3 by default. (#5159)
|
||||
- Dev: Move `clang-tidy` checker to its own CI job. (#4996)
|
||||
- Dev: Refactored the Image Uploader feature. (#4971)
|
||||
- Dev: Refactored the SplitOverlay code. (#5082)
|
||||
- Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096)
|
||||
- Dev: Refactored emotes out of TwitchIrcServer. (#5120)
|
||||
- Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096, #5144)
|
||||
- Dev: Refactored emotes out of TwitchIrcServer. (#5120, #5146)
|
||||
- Dev: Refactored the ChatterinoBadges structure, making it less of a singleton. (#5103)
|
||||
- Dev: Refactored the ColorProvider class a bit. (#5112)
|
||||
- Dev: Moved the Network files to their own folder. (#5089)
|
||||
|
@ -123,11 +147,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: Fixed most compiler warnings. (#5028)
|
||||
- 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: Changed Ubuntu & AppImage builders to statically link Qt. (#5151)
|
||||
- Dev: Refactor `NetworkPrivate`. (#5063)
|
||||
- Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102)
|
||||
- Dev: Removed duplicate scale in settings dialog. (#5069)
|
||||
|
@ -136,6 +161,7 @@
|
|||
- Dev: Boost is depended on as a header-only library when using conan. (#5107)
|
||||
- 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)
|
||||
|
||||
## 2.4.6
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ option(BUILD_WITH_QTKEYCHAIN "Build Chatterino with support for your system key
|
|||
option(USE_SYSTEM_MINIAUDIO "Build Chatterino with your system miniaudio" OFF)
|
||||
option(BUILD_WITH_CRASHPAD "Build chatterino with crashpad" OFF)
|
||||
option(USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
|
||||
option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF)
|
||||
if(WIN32)
|
||||
option(BUILD_WITH_QT6 "Build with Qt6, default on for Windows" On)
|
||||
else()
|
||||
option(BUILD_WITH_QT6 "Use Qt6 instead of default Qt5" OFF)
|
||||
endif()
|
||||
option(CHATTERINO_GENERATE_COVERAGE "Generate coverage files" OFF)
|
||||
# We don't use translations, and we don't want qtkeychain to build translations
|
||||
option(BUILD_TRANSLATIONS "" OFF)
|
||||
|
|
|
@ -32,3 +32,10 @@ set_target_properties(${PROJECT_NAME}
|
|||
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin"
|
||||
AUTORCC ON
|
||||
)
|
||||
|
||||
if (CHATTERINO_STATIC_QT_BUILD)
|
||||
qt_import_plugins(${PROJECT_NAME} INCLUDE_BY_TYPE
|
||||
platforms Qt::QXcbIntegrationPlugin
|
||||
Qt::QMinimalIntegrationPlugin
|
||||
)
|
||||
endif ()
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include "providers/recentmessages/Impl.hpp"
|
||||
#include "providers/seventv/SeventvBadges.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
|
@ -72,6 +73,26 @@ public:
|
|||
return &this->highlights;
|
||||
}
|
||||
|
||||
TwitchBadges *getTwitchBadges() override
|
||||
{
|
||||
return &this->twitchBadges;
|
||||
}
|
||||
|
||||
BttvEmotes *getBttvEmotes() override
|
||||
{
|
||||
return &this->bttvEmotes;
|
||||
}
|
||||
|
||||
FfzEmotes *getFfzEmotes() override
|
||||
{
|
||||
return &this->ffzEmotes;
|
||||
}
|
||||
|
||||
SeventvEmotes *getSeventvEmotes() override
|
||||
{
|
||||
return &this->seventvEmotes;
|
||||
}
|
||||
|
||||
AccountController accounts;
|
||||
Emotes emotes;
|
||||
mock::UserDataController userData;
|
||||
|
@ -80,6 +101,10 @@ public:
|
|||
FfzBadges ffzBadges;
|
||||
SeventvBadges seventvBadges;
|
||||
HighlightController highlights;
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttvEmotes;
|
||||
FfzEmotes ffzEmotes;
|
||||
SeventvEmotes seventvEmotes;
|
||||
};
|
||||
|
||||
std::optional<QJsonDocument> tryReadJsonFile(const QString &path)
|
||||
|
|
|
@ -9,7 +9,7 @@ class Chatterino(ConanFile):
|
|||
settings = "os", "compiler", "build_type", "arch"
|
||||
default_options = {
|
||||
"with_benchmark": False,
|
||||
"with_openssl3": False,
|
||||
"with_openssl3": True,
|
||||
"openssl*:shared": True,
|
||||
"boost*:header_only": True,
|
||||
}
|
||||
|
|
|
@ -339,6 +339,8 @@
|
|||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"liveIndicator": { "$ref": "#/definitions/qt-color" },
|
||||
"rerunIndicator": { "$ref": "#/definitions/qt-color" },
|
||||
"dividerLine": { "$ref": "#/definitions/qt-color" },
|
||||
"highlighted": {
|
||||
"$ref": "#/definitions/tab-colors"
|
||||
|
@ -388,6 +390,11 @@
|
|||
"$comment": "Determines which icons to use. 'dark' will use dark icons (best for a light theme). 'light' will use light icons.",
|
||||
"enum": ["light", "dark"],
|
||||
"default": "light"
|
||||
},
|
||||
"fallbackTheme": {
|
||||
"$comment": "Determines which built-in Chatterino theme to use as a fallback in case a color isn't configured.",
|
||||
"enum": ["White", "Light", "Dark", "Black"],
|
||||
"default": "Dark"
|
||||
}
|
||||
},
|
||||
"required": ["iconTheme"]
|
||||
|
|
62
docs/chatterino.d.ts
vendored
62
docs/chatterino.d.ts
vendored
|
@ -9,7 +9,64 @@ declare module c2 {
|
|||
}
|
||||
class CommandContext {
|
||||
words: String[];
|
||||
channel_name: String;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
Twitch,
|
||||
}
|
||||
enum ChannelType {
|
||||
None,
|
||||
Direct,
|
||||
Twitch,
|
||||
TwitchWhispers,
|
||||
TwitchWatching,
|
||||
TwitchMentions,
|
||||
TwitchLive,
|
||||
TwitchAutomod,
|
||||
Irc,
|
||||
Misc,
|
||||
}
|
||||
|
||||
interface IWeakResource {
|
||||
is_valid(): boolean;
|
||||
}
|
||||
|
||||
class RoomModes {
|
||||
unique_chat: boolean;
|
||||
subscriber_only: boolean;
|
||||
emotes_only: boolean;
|
||||
follower_only: null | number;
|
||||
slow_mode: null | number;
|
||||
}
|
||||
class StreamStatus {
|
||||
live: boolean;
|
||||
viewer_count: number;
|
||||
uptime: number;
|
||||
title: string;
|
||||
game_name: string;
|
||||
game_id: string;
|
||||
}
|
||||
|
||||
class Channel implements IWeakResource {
|
||||
is_valid(): boolean;
|
||||
get_name(): string;
|
||||
get_type(): ChannelType;
|
||||
get_display_name(): string;
|
||||
send_message(message: string, execute_commands: boolean): void;
|
||||
add_system_message(message: string): void;
|
||||
|
||||
is_twitch_channel(): boolean;
|
||||
|
||||
get_room_modes(): RoomModes;
|
||||
get_stream_status(): StreamStatus;
|
||||
get_twitch_id(): string;
|
||||
is_broadcaster(): boolean;
|
||||
is_mod(): boolean;
|
||||
is_vip(): boolean;
|
||||
|
||||
static by_name(name: string, platform: Platform): null | Channel;
|
||||
static by_twitch_id(id: string): null | Channel;
|
||||
}
|
||||
|
||||
function log(level: LogLevel, ...data: any[]): void;
|
||||
|
@ -17,8 +74,6 @@ declare module c2 {
|
|||
name: String,
|
||||
handler: (ctx: CommandContext) => void
|
||||
): boolean;
|
||||
function send_msg(channel: String, text: String): boolean;
|
||||
function system_msg(channel: String, text: String): boolean;
|
||||
|
||||
class CompletionList {
|
||||
values: String[];
|
||||
|
@ -40,4 +95,5 @@ declare module c2 {
|
|||
: never;
|
||||
|
||||
function register_callback<T>(type: T, func: CbFunc<T>): void;
|
||||
function later(callback: () => void, msec: number): void;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,15 @@
|
|||
|
||||
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 }
|
||||
c2.LogLevel = {}
|
||||
|
@ -15,11 +24,147 @@ c2.LogLevel = {}
|
|||
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_name string The name of the channel the command was executed in.
|
||||
---@field channel Channel The channel the command was executed in.
|
||||
|
||||
---@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.
|
||||
|
||||
---@alias ChannelType integer
|
||||
---@type { None: 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.
|
||||
|
||||
---@alias Platform integer
|
||||
---@type { Twitch: Platform }
|
||||
Platform = {}
|
||||
---@class Channel: 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 success
|
||||
function Channel:is_valid() end
|
||||
|
||||
--- Gets the channel's name. This is the lowercase login name.
|
||||
---
|
||||
---@return string name
|
||||
function Channel:get_name() end
|
||||
|
||||
--- Gets the channel's type
|
||||
---
|
||||
---@return ChannelType
|
||||
function Channel:get_type() end
|
||||
|
||||
--- Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||
---
|
||||
---@return string name
|
||||
function Channel:get_display_name() end
|
||||
|
||||
--- Sends a message to the target channel.
|
||||
--- Note that this does not execute client-commands.
|
||||
---
|
||||
---@param message string
|
||||
---@param execute_commands boolean Should commands be run on the text?
|
||||
function Channel:send_message(message, execute_commands) end
|
||||
|
||||
--- Adds a system message client-side
|
||||
---
|
||||
---@param message string
|
||||
function Channel:add_system_message(message) end
|
||||
|
||||
--- Returns true for twitch channels.
|
||||
--- Compares the channel Type. Note that enum values aren't guaranteed, just
|
||||
--- that they are equal to the exposed enum.
|
||||
---
|
||||
---@return bool
|
||||
function Channel:is_twitch_channel() end
|
||||
|
||||
--- Twitch Channel specific functions
|
||||
|
||||
--- Returns a copy of the channel mode settings (subscriber only, r9k etc.)
|
||||
---
|
||||
---@return RoomModes
|
||||
function Channel:get_room_modes() end
|
||||
|
||||
--- Returns a copy of the stream status.
|
||||
---
|
||||
---@return StreamStatus
|
||||
function Channel:get_stream_status() end
|
||||
|
||||
--- Returns the Twitch user ID of the owner of the channel.
|
||||
---
|
||||
---@return string
|
||||
function Channel:get_twitch_id() end
|
||||
|
||||
--- Returns true if the channel is a Twitch channel and the user owns it
|
||||
---
|
||||
---@return boolean
|
||||
function Channel:is_broadcaster() end
|
||||
|
||||
--- Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||
--- Returns false for broadcaster.
|
||||
---
|
||||
---@return boolean
|
||||
function Channel:is_mod() end
|
||||
|
||||
--- Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||
--- Returns false for broadcaster.
|
||||
---
|
||||
---@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
|
||||
--- - /watching
|
||||
--- - /live
|
||||
--- - /automod
|
||||
---
|
||||
---@param name string Which channel are you looking for?
|
||||
---@param platform Platform Where to search for the channel?
|
||||
---@return Channel?
|
||||
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.
|
||||
---@return Channel?
|
||||
function Channel.by_twitch_id(string) 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 slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||
|
||||
---@class StreamStatus
|
||||
---@field live boolean
|
||||
---@field viewer_count number
|
||||
---@field uptime number Seconds since the stream started.
|
||||
---@field title string Stream title or last stream title
|
||||
---@field game_name string
|
||||
---@field game_id string
|
||||
-- Back to src/controllers/plugins/LuaAPI.hpp.
|
||||
|
||||
--- Registers a new command called `name` which when executed will call `handler`.
|
||||
---
|
||||
|
@ -34,25 +179,15 @@ function c2.register_command(name, handler) end
|
|||
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
|
||||
function c2.register_callback(type, func) end
|
||||
|
||||
--- Sends a message to `channel` with the specified text. Also executes commands.
|
||||
---
|
||||
--- **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
---
|
||||
---@param channel string The name of the Twitch channel
|
||||
---@param text string The text to be sent
|
||||
---@return boolean ok
|
||||
function c2.send_msg(channel, text) end
|
||||
|
||||
--- Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
||||
---
|
||||
---@param channel string
|
||||
---@param text string
|
||||
---@return boolean ok
|
||||
function c2.system_msg(channel, text) end
|
||||
|
||||
--- Writes a message to the Chatterino log.
|
||||
---
|
||||
---@param level LogLevel The desired level.
|
||||
---@param ... any Values to log. Should be convertible to a string with `tostring()`.
|
||||
function c2.log(level, ...) end
|
||||
|
||||
--- Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
||||
---
|
||||
---@param callback fun() The callback that will be called.
|
||||
---@param msec number How long to wait.
|
||||
function c2.later(callback, msec) end
|
||||
|
||||
|
|
|
@ -48,6 +48,13 @@ to typecheck your plugins. There is a `chatterino.d.ts` file describing the API
|
|||
in this directory. However, this has several drawbacks like harder debugging at
|
||||
runtime.
|
||||
|
||||
## LuaLS type definitions
|
||||
|
||||
Type definitions for LuaLS are available in
|
||||
[the `/plugin-meta.lua` file](./plugin-meta.lua). These are generated from [the C++
|
||||
headers](../src/controllers/plugins/LuaAPI.hpp) of Chatterino using [a
|
||||
script](../scripts/make_luals_meta.py).
|
||||
|
||||
## API
|
||||
|
||||
The following parts of the Lua standard library are loaded:
|
||||
|
@ -97,14 +104,14 @@ command with this name.
|
|||
Example:
|
||||
|
||||
```lua
|
||||
function cmdWords(ctx)
|
||||
function cmd_words(ctx)
|
||||
-- ctx contains:
|
||||
-- words - table of words supplied to the command including the trigger
|
||||
-- channel_name - name of the channel the command is being run in
|
||||
c2.system_msg(ctx.channel_name, "Words are: " .. table.concat(ctx.words, " "))
|
||||
-- channel - the channel the command is being run in
|
||||
channel:add_system_message("Words are: " .. table.concat(ctx.words, " "))
|
||||
end
|
||||
|
||||
c2.register_command("/words", cmdWords)
|
||||
c2.register_command("/words", cmd_words)
|
||||
```
|
||||
|
||||
Limitations/known issues:
|
||||
|
@ -149,41 +156,175 @@ c2.register_callback(
|
|||
)
|
||||
```
|
||||
|
||||
#### `send_msg(channel, text)`
|
||||
#### `Platform` enum
|
||||
|
||||
Sends a message to `channel` with the specified text. Also executes commands.
|
||||
This table describes platforms that can be accessed. Chatterino supports IRC
|
||||
however plugins do not yet have explicit access to get IRC channels objects.
|
||||
The values behind the names may change, do not count on them. It has the
|
||||
following keys:
|
||||
|
||||
- `Twitch`
|
||||
|
||||
#### `ChannelType` enum
|
||||
|
||||
This table describes channel types Chatterino supports. The values behind the
|
||||
names may change, do not count on them. It has the following keys:
|
||||
|
||||
- `None`
|
||||
- `Direct`
|
||||
- `Twitch`
|
||||
- `TwitchWhispers`
|
||||
- `TwitchWatching`
|
||||
- `TwitchMentions`
|
||||
- `TwitchLive`
|
||||
- `TwitchAutomod`
|
||||
- `TwitchEnd`
|
||||
- `Irc`
|
||||
- `Misc`
|
||||
|
||||
#### `Channel`
|
||||
|
||||
This is a type that represents a channel. Existence of this object doesn't
|
||||
force Chatterino to hold the channel open. Should the user close the last split
|
||||
holding this channel open, your Channel object will expire. You can check for
|
||||
this using the `Channel:is_valid()` function. Using any other function on an
|
||||
expired Channel yields an error. Using any `Channel` member function on a
|
||||
non-`Channel` table also yields an error.
|
||||
|
||||
Some functions make sense only for Twitch channel, these yield an error when
|
||||
used on non-Twitch channels. Special channels while marked as
|
||||
`is_twitch_channel() = true` do not have these functions. To check if a channel
|
||||
is an actual Twitch chatroom use `Channel:get_type()` instead of
|
||||
`Channel:is_twitch_channel()`.
|
||||
|
||||
##### `Channel:by_name(name, platform)`
|
||||
|
||||
Finds a channel given by `name` on `platform` (see [`Platform` enum](#Platform-enum)). Returns the channel or `nil` if not open.
|
||||
|
||||
Some miscellaneous channels are marked as if they are specifically Twitch channels:
|
||||
|
||||
- `/whispers`
|
||||
- `/mentions`
|
||||
- `/watching`
|
||||
- `/live`
|
||||
- `/automod`
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
function cmdShout(ctx)
|
||||
local pajladas = c2.Channel.by_name("pajlada", c2.Platform.Twitch)
|
||||
```
|
||||
|
||||
##### `Channel:by_twitch_id(id)`
|
||||
|
||||
Finds a channel given by the string representation of the owner's Twitch user ID. Returns the channel or `nil` if not open.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
local pajladas = c2.Channel.by_twitch_id("11148817")
|
||||
```
|
||||
|
||||
##### `Channel:get_name()`
|
||||
|
||||
On Twitch returns the lowercase login name of the channel owner. On IRC returns the normalized channel name.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
-- Note: if the channel is not open this errors
|
||||
pajladas:get_name() -- "pajlada"
|
||||
```
|
||||
|
||||
##### `Channel:get_type()`
|
||||
|
||||
Returns the channel's type. See [`ChannelType` enum](#ChannelType-enum).
|
||||
|
||||
##### `Channel:get_display_name()`
|
||||
|
||||
Returns the channel owner's display name. This can contain characters that are not lowercase and even non-ASCII.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
local saddummys = c2.Channel.by_name("saddummy")
|
||||
saddummys:get_display_name() -- "서새봄냥"
|
||||
```
|
||||
|
||||
<!-- F Korean Twitch, apparently you were not profitable enough -->
|
||||
|
||||
##### `Channel:send_message(message[, execute_commands])`
|
||||
|
||||
Sends a message to the channel with the given text. If `execute_commands` is
|
||||
not present or `false` commands will not be executed client-side, this affects
|
||||
all user commands and all Twitch commands except `/me`.
|
||||
|
||||
Examples:
|
||||
|
||||
```lua
|
||||
-- times out @Mm2PL
|
||||
pajladas:send_message("/timeout mm2pl 1s test", true)
|
||||
|
||||
-- results in a "Unknown command" error from Twitch
|
||||
pajladas:send_message("/timeout mm2pl 1s test")
|
||||
|
||||
-- Given a user command "hello":
|
||||
-- this will execute it
|
||||
pajladas:send_message("hello", true)
|
||||
-- this will send "hello" literally, bypassing commands
|
||||
pajladas:send_message("hello")
|
||||
|
||||
function cmd_shout(ctx)
|
||||
table.remove(ctx.words, 1)
|
||||
local output = table.concat(ctx.words, " ")
|
||||
c2.send_msg(ctx.channel_name, string.upper(output))
|
||||
ctx.channel:send_message(string.upper(output))
|
||||
end
|
||||
c2.register_command("/shout", cmdShout)
|
||||
c2.register_command("/shout", cmd_shout)
|
||||
```
|
||||
|
||||
Limitations/Known issues:
|
||||
|
||||
- It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
|
||||
#### `system_msg(channel, text)`
|
||||
##### `Channel:add_system_message(message)`
|
||||
|
||||
Creates a system message and adds it to the twitch channel specified by
|
||||
`channel`. Returns `true` if everything went ok, `false` otherwise. It will
|
||||
throw an error if the number of arguments received doesn't match what it
|
||||
expects.
|
||||
Shows a system message in the channel with the given text.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
local ok = c2.system_msg("pajlada", "test")
|
||||
if (not ok)
|
||||
-- channel not found
|
||||
end
|
||||
pajladas:add_system_message("Hello, world!")
|
||||
```
|
||||
|
||||
##### `Channel:is_twitch_channel()`
|
||||
|
||||
Returns `true` if the channel is a Twitch channel, that is its type name has
|
||||
the `Twitch` prefix. This returns `true` for special channels like Mentions.
|
||||
You might want `Channel:get_type() == "Twitch"` if you want to use
|
||||
Twitch-specific functions.
|
||||
|
||||
##### `Channel:get_twitch_id()`
|
||||
|
||||
Returns the string form of the channel owner's Twitch user ID.
|
||||
|
||||
Example:
|
||||
|
||||
```lua
|
||||
pajladas:get_twitch_id() -- "11148817"
|
||||
```
|
||||
|
||||
##### `Channel:is_broadcaster()`
|
||||
|
||||
Returns `true` if the channel is owned by the current user.
|
||||
|
||||
##### `Channel:is_mod()`
|
||||
|
||||
Returns `true` if the channel can be moderated by the current user.
|
||||
|
||||
##### `Channel:is_vip()`
|
||||
|
||||
Returns `true` if the current user is a VIP in the channel.
|
||||
|
||||
### Changed globals
|
||||
|
||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 7923dbbf72da303ca1cca17efd24725668992f15
|
||||
Subproject commit e288c5a91883793d14ed9e9d93464f6ee0b08915
|
|
@ -1 +1 @@
|
|||
Subproject commit 53be0788a000960dbbc34350315e20ad1e194970
|
||||
Subproject commit b7918804e37ebba092e5a26daedd72931b96ce05
|
|
@ -186,6 +186,15 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginController *getPlugins() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getPlugins was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
Updates &getUpdates() override
|
||||
{
|
||||
return this->updates_;
|
||||
|
@ -212,6 +221,13 @@ public:
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
ILinkResolver *getLinkResolver() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getLinkResolver was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Paths paths_;
|
||||
Args args_;
|
||||
|
|
|
@ -24,8 +24,14 @@ public:
|
|||
return this->watchingChannel;
|
||||
}
|
||||
|
||||
QString getLastUserThatWhisperedMe() const override
|
||||
{
|
||||
return this->lastUserThatWhisperedMe;
|
||||
}
|
||||
|
||||
ChannelPtr watchingChannelInner;
|
||||
IndirectChannel watchingChannel;
|
||||
QString lastUserThatWhisperedMe{"forsen"};
|
||||
};
|
||||
|
||||
} // namespace chatterino::mock
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 456 KiB |
|
@ -8,6 +8,7 @@
|
|||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<name>Chatterino</name>
|
||||
<developer_name>Chatterino Developers</developer_name>
|
||||
<summary>
|
||||
Chat client for twitch.tv
|
||||
</summary>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -50,6 +50,8 @@
|
|||
"resizeHandleBackground": "#200094ff"
|
||||
},
|
||||
"tabs": {
|
||||
"liveIndicator": "#ff0000",
|
||||
"rerunIndicator": "#c7c715",
|
||||
"dividerLine": "#555555",
|
||||
"highlighted": {
|
||||
"backgrounds": {
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
"resizeHandleBackground": "#200094ff"
|
||||
},
|
||||
"tabs": {
|
||||
"liveIndicator": "#ff0000",
|
||||
"rerunIndicator": "#c7c715",
|
||||
"dividerLine": "#555555",
|
||||
"highlighted": {
|
||||
"backgrounds": {
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
"resizeHandleBackground": "#500094ff"
|
||||
},
|
||||
"tabs": {
|
||||
"liveIndicator": "#ff0000",
|
||||
"rerunIndicator": "#c7c715",
|
||||
"dividerLine": "#b4d7ff",
|
||||
"highlighted": {
|
||||
"backgrounds": {
|
||||
|
|
|
@ -50,6 +50,8 @@
|
|||
"resizeHandleBackground": "#500094ff"
|
||||
},
|
||||
"tabs": {
|
||||
"liveIndicator": "#ff0000",
|
||||
"rerunIndicator": "#c7c715",
|
||||
"dividerLine": "#b4d7ff",
|
||||
"highlighted": {
|
||||
"backgrounds": {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -12,6 +12,8 @@ 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
|
||||
|
||||
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
|
||||
|
@ -38,42 +40,54 @@ BOILERPLATE = """
|
|||
-- Add the folder this file is in to "Lua.workspace.library".
|
||||
|
||||
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
|
||||
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
|
||||
lua_meta = repo_root / "docs" / "plugin-meta.lua"
|
||||
|
||||
print("Reading from", lua_api_file.relative_to(repo_root))
|
||||
print("Writing to", lua_meta.relative_to(repo_root))
|
||||
with lua_api_file.open("r") as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
# Are we in a doc comment?
|
||||
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
|
||||
def process_file(target, out):
|
||||
print("Reading from", target.relative_to(repo_root))
|
||||
with target.open("r") as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
with lua_meta.open("w") as out:
|
||||
out.write(BOILERPLATE[1:]) # skip the newline after triple quote
|
||||
# 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
|
||||
|
||||
for line in lines:
|
||||
# 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"Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
|
||||
f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
|
||||
)
|
||||
current_enum_name = None
|
||||
continue
|
||||
|
@ -94,7 +108,7 @@ with lua_meta.open("w") as out:
|
|||
out.write(", ")
|
||||
out.write(entry + ": " + current_enum_name)
|
||||
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
|
||||
print(f"Wrote enum {expose_next_enum_as} => {current_enum_name}")
|
||||
print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}")
|
||||
current_enum_name = None
|
||||
expose_next_enum_as = None
|
||||
continue
|
||||
|
@ -104,28 +118,40 @@ with lua_meta.open("w") as out:
|
|||
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"Wrote function {exp}(...)")
|
||||
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"Writing {command}")
|
||||
print(f"{loc} Writing {command}")
|
||||
if is_class:
|
||||
out.write("\n")
|
||||
is_class = True
|
||||
|
@ -140,3 +166,8 @@ with lua_meta.open("w") as out:
|
|||
|
||||
# note the space difference from the branch above
|
||||
out.write("--- " + line + "\n")
|
||||
|
||||
|
||||
with lua_meta.open("w") as output:
|
||||
output.write(BOILERPLATE[1:]) # skip the newline after triple quote
|
||||
process_file(lua_api_file, output)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
#include "controllers/sound/ISoundController.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/links/LinkResolver.hpp"
|
||||
#include "providers/seventv/SeventvAPI.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
@ -142,6 +143,7 @@ Application::Application(Settings &_settings, const Paths &paths,
|
|||
, ffzEmotes(new FfzEmotes)
|
||||
, seventvEmotes(new SeventvEmotes)
|
||||
, logging(new Logging(_settings))
|
||||
, linkResolver(new LinkResolver)
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(&this->emplace(new PluginController(paths)))
|
||||
#endif
|
||||
|
@ -204,6 +206,10 @@ void Application::initialize(Settings &settings, const Paths &paths)
|
|||
singleton->initialize(settings, paths);
|
||||
}
|
||||
|
||||
// XXX: Loading Twitch badges after Helix has been initialized, which only happens after
|
||||
// the AccountController initialize has been called
|
||||
this->twitchBadges->loadTwitchBadges();
|
||||
|
||||
// Show crash message.
|
||||
// On Windows, the crash message was already shown.
|
||||
#ifndef Q_OS_WIN
|
||||
|
@ -490,6 +496,13 @@ Logging *Application::getChatLogger()
|
|||
return this->logging.get();
|
||||
}
|
||||
|
||||
ILinkResolver *Application::getLinkResolver()
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
||||
return this->linkResolver.get();
|
||||
}
|
||||
|
||||
BttvEmotes *Application::getBttvEmotes()
|
||||
{
|
||||
assertInGuiThread();
|
||||
|
|
|
@ -54,6 +54,7 @@ class CrashHandler;
|
|||
class BttvEmotes;
|
||||
class FfzEmotes;
|
||||
class SeventvEmotes;
|
||||
class ILinkResolver;
|
||||
|
||||
class IApplication
|
||||
{
|
||||
|
@ -95,6 +96,7 @@ public:
|
|||
virtual BttvEmotes *getBttvEmotes() = 0;
|
||||
virtual FfzEmotes *getFfzEmotes() = 0;
|
||||
virtual SeventvEmotes *getSeventvEmotes() = 0;
|
||||
virtual ILinkResolver *getLinkResolver() = 0;
|
||||
};
|
||||
|
||||
class Application : public IApplication
|
||||
|
@ -162,6 +164,7 @@ private:
|
|||
std::unique_ptr<FfzEmotes> ffzEmotes;
|
||||
std::unique_ptr<SeventvEmotes> seventvEmotes;
|
||||
const std::unique_ptr<Logging> logging;
|
||||
std::unique_ptr<ILinkResolver> linkResolver;
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
PluginController *const plugins{};
|
||||
#endif
|
||||
|
@ -212,6 +215,8 @@ public:
|
|||
FfzEmotes *getFfzEmotes() override;
|
||||
SeventvEmotes *getSeventvEmotes() override;
|
||||
|
||||
ILinkResolver *getLinkResolver() override;
|
||||
|
||||
pajlada::Signals::NoArgSignal streamerModeChanged;
|
||||
|
||||
private:
|
||||
|
|
|
@ -5,6 +5,7 @@ add_compile_definitions(QT_DISABLE_DEPRECATED_BEFORE=0x050F00)
|
|||
|
||||
# registers the native messageing host
|
||||
option(CHATTERINO_DEBUG_NATIVE_MESSAGES "Debug native messages" OFF)
|
||||
option(CHATTERINO_STATIC_QT_BUILD "Static link Qt" OFF)
|
||||
|
||||
set(SOURCE_FILES
|
||||
Application.cpp
|
||||
|
@ -220,6 +221,8 @@ set(SOURCE_FILES
|
|||
controllers/pings/MutedChannelModel.cpp
|
||||
controllers/pings/MutedChannelModel.hpp
|
||||
|
||||
controllers/plugins/api/ChannelRef.cpp
|
||||
controllers/plugins/api/ChannelRef.hpp
|
||||
controllers/plugins/LuaAPI.cpp
|
||||
controllers/plugins/LuaAPI.hpp
|
||||
controllers/plugins/Plugin.cpp
|
||||
|
@ -294,8 +297,6 @@ set(SOURCE_FILES
|
|||
|
||||
providers/IvrApi.cpp
|
||||
providers/IvrApi.hpp
|
||||
providers/LinkResolver.cpp
|
||||
providers/LinkResolver.hpp
|
||||
providers/NetworkConfigurationProvider.cpp
|
||||
providers/NetworkConfigurationProvider.hpp
|
||||
|
||||
|
@ -342,6 +343,11 @@ set(SOURCE_FILES
|
|||
providers/irc/IrcServer.cpp
|
||||
providers/irc/IrcServer.hpp
|
||||
|
||||
providers/links/LinkInfo.cpp
|
||||
providers/links/LinkInfo.hpp
|
||||
providers/links/LinkResolver.cpp
|
||||
providers/links/LinkResolver.hpp
|
||||
|
||||
providers/liveupdates/BasicPubSubClient.hpp
|
||||
providers/liveupdates/BasicPubSubManager.hpp
|
||||
providers/liveupdates/BasicPubSubWebsocket.hpp
|
||||
|
@ -729,7 +735,7 @@ add_library(${LIBRARY_PROJECT} OBJECT ${SOURCE_FILES})
|
|||
|
||||
if(CHATTERINO_PLUGINS)
|
||||
target_compile_definitions(${LIBRARY_PROJECT}
|
||||
PRIVATE
|
||||
PUBLIC
|
||||
CHATTERINO_HAVE_PLUGINS
|
||||
)
|
||||
message(STATUS "Building Chatterino with lua plugin support enabled.")
|
||||
|
@ -816,6 +822,13 @@ if (BUILD_APP)
|
|||
message(WARNING "Sanitizers support is disabled")
|
||||
endif()
|
||||
|
||||
if (CHATTERINO_STATIC_QT_BUILD)
|
||||
qt_import_plugins(${EXECUTABLE_PROJECT} INCLUDE_BY_TYPE
|
||||
platforms Qt::QXcbIntegrationPlugin
|
||||
Qt::QMinimalIntegrationPlugin
|
||||
)
|
||||
endif ()
|
||||
|
||||
target_include_directories(${EXECUTABLE_PROJECT} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/)
|
||||
|
||||
target_link_libraries(${EXECUTABLE_PROJECT} PUBLIC ${LIBRARY_PROJECT})
|
||||
|
@ -946,6 +959,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
|
|||
AB_CUSTOM_SETTINGS
|
||||
IRC_STATIC
|
||||
IRC_NAMESPACE=Communi
|
||||
$<$<BOOL:${WIN32}>:_WIN32_WINNT=0x0A00> # Windows 10
|
||||
)
|
||||
|
||||
if (USE_SYSTEM_QTKEYCHAIN)
|
||||
|
@ -1059,10 +1073,6 @@ if (MSVC)
|
|||
# Someone adds /W3 before we add /W4.
|
||||
# This makes sure, only /W4 is specified.
|
||||
string(REPLACE "/W3" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
# 4505 - "unreferenced local version has been removed"
|
||||
# Although this might give hints on dead code,
|
||||
# there are some cases where it's distracting.
|
||||
#
|
||||
# 4100 - "unreferenced formal parameter"
|
||||
# There are a lot of functions and methods where
|
||||
# an argument was given a name but never used.
|
||||
|
@ -1073,6 +1083,11 @@ if (MSVC)
|
|||
# These are implicit conversions from size_t to int/qsizetype.
|
||||
# We don't use size_t in a lot of cases, since
|
||||
# Qt doesn't use it - it uses int (or qsizetype in Qt6).
|
||||
#
|
||||
# 4458 - "declaration of 'identifier' hides class member"
|
||||
# We have a rule of exclusively using `this->`
|
||||
# to access class members, thus it's fine to reclare a variable
|
||||
# with the same name as a class member.
|
||||
target_compile_options(${LIBRARY_PROJECT} PUBLIC
|
||||
/W4
|
||||
# 5038 - warnings about initialization order
|
||||
|
@ -1080,9 +1095,9 @@ if (MSVC)
|
|||
# 4855 - implicit capture of 'this' via '[=]' is deprecated
|
||||
/w14855
|
||||
# Disable the following warnings (see reasoning above)
|
||||
/wd4505
|
||||
/wd4100
|
||||
/wd4267
|
||||
/wd4458
|
||||
# Enable updated '__cplusplus' macro - workaround for CMake#18837
|
||||
/Zc:__cplusplus
|
||||
)
|
||||
|
|
|
@ -78,6 +78,14 @@ namespace {
|
|||
{
|
||||
// set up the QApplication flags
|
||||
QApplication::setAttribute(Qt::AA_Use96Dpi, true);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Avoid promoting child widgets to child windows
|
||||
// This causes bugs with frameless windows as not all child events
|
||||
// get sent to the parent - effectively making the window immovable.
|
||||
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
|
||||
|
|
|
@ -82,7 +82,6 @@ LimitedQueueSnapshot<MessagePtr> Channel::getMessageSnapshot()
|
|||
void Channel::addMessage(MessagePtr message,
|
||||
std::optional<MessageFlags> overridingFlags)
|
||||
{
|
||||
auto *app = getApp();
|
||||
MessagePtr deleted;
|
||||
|
||||
if (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))
|
||||
|
@ -329,6 +328,11 @@ bool Channel::isLive() const
|
|||
return false;
|
||||
}
|
||||
|
||||
bool Channel::isRerun() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Channel::shouldIgnoreHighlights() const
|
||||
{
|
||||
return this->type_ == Type::TwitchAutomod ||
|
||||
|
|
|
@ -30,6 +30,10 @@ enum class TimeoutStackStyle : int {
|
|||
class Channel : public std::enable_shared_from_this<Channel>
|
||||
{
|
||||
public:
|
||||
// This is for Lua. See scripts/make_luals_meta.py
|
||||
/**
|
||||
* @exposeenum ChannelType
|
||||
*/
|
||||
enum class Type {
|
||||
None,
|
||||
Direct,
|
||||
|
@ -100,6 +104,7 @@ public:
|
|||
virtual bool hasModRights() const;
|
||||
virtual bool hasHighRateLimit() const;
|
||||
virtual bool isLive() const;
|
||||
virtual bool isRerun() const;
|
||||
virtual bool shouldIgnoreHighlights() const;
|
||||
virtual bool canReconnect() const;
|
||||
virtual void reconnect();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "common/Credentials.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Modes.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -41,7 +42,7 @@ bool useKeyring()
|
|||
#ifdef NO_QTKEYCHAIN
|
||||
return false;
|
||||
#endif
|
||||
if (getIApp()->getPaths().isPortable())
|
||||
if (Modes::instance().isPortable)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include "widgets/splits/SplitContainer.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QDesktopServices>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
@ -414,26 +415,97 @@ QString clearmessages(const CommandContext &ctx)
|
|||
|
||||
QString openURL(const CommandContext &ctx)
|
||||
{
|
||||
/**
|
||||
* The /openurl command
|
||||
* Takes a positional argument as the URL to open
|
||||
*
|
||||
* Accepts the option --private or --no-private (or --incognito or --no-incognito).
|
||||
* These options will force the URL to be opened in private or non-private mode, regardless of the
|
||||
* default incognito mode setting.
|
||||
*
|
||||
* Examples:
|
||||
* - /openurl https://twitch.tv/forsen
|
||||
* with the setting "Open links in incognito/private mode" enabled
|
||||
* Opens https://twitch.tv/forsen in private mode
|
||||
* - /openurl https://twitch.tv/forsen
|
||||
* with the setting "Open links in incognito/private mode" disabled
|
||||
* Opens https://twitch.tv/forsen in normal mode
|
||||
* - /openurl https://twitch.tv/forsen --private
|
||||
* with the setting "Open links in incognito/private mode" disabled
|
||||
* Opens https://twitch.tv/forsen in private mode
|
||||
* - /openurl https://twitch.tv/forsen --no-private
|
||||
* with the setting "Open links in incognito/private mode" enabled
|
||||
* Opens https://twitch.tv/forsen in normal mode
|
||||
*/
|
||||
if (ctx.channel == nullptr)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ctx.words.size() < 2)
|
||||
QCommandLineParser parser;
|
||||
parser.setOptionsAfterPositionalArgumentsMode(
|
||||
QCommandLineParser::ParseAsPositionalArguments);
|
||||
parser.addPositionalArgument("URL", "The URL to open");
|
||||
QCommandLineOption privateModeOption(
|
||||
{
|
||||
"private",
|
||||
"incognito",
|
||||
},
|
||||
"Force private mode. Cannot be used together with --no-private");
|
||||
QCommandLineOption noPrivateModeOption(
|
||||
{
|
||||
"no-private",
|
||||
"no-incognito",
|
||||
},
|
||||
"Force non-private mode. Cannot be used together with --private");
|
||||
parser.addOptions({
|
||||
privateModeOption,
|
||||
noPrivateModeOption,
|
||||
});
|
||||
parser.parse(ctx.words);
|
||||
|
||||
const auto &positionalArguments = parser.positionalArguments();
|
||||
if (positionalArguments.isEmpty())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage("Usage: /openurl <URL>"));
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"Usage: /openurl <URL> [--incognito/--no-incognito]"));
|
||||
return "";
|
||||
}
|
||||
auto urlString = parser.positionalArguments().join(' ');
|
||||
|
||||
QUrl url = QUrl::fromUserInput(ctx.words.mid(1).join(" "));
|
||||
QUrl url = QUrl::fromUserInput(urlString);
|
||||
if (!url.isValid())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage("Invalid URL specified."));
|
||||
return "";
|
||||
}
|
||||
|
||||
auto preferPrivateMode = getSettings()->openLinksIncognito.getValue();
|
||||
auto forcePrivateMode = parser.isSet(privateModeOption);
|
||||
auto forceNonPrivateMode = parser.isSet(noPrivateModeOption);
|
||||
|
||||
if (forcePrivateMode && forceNonPrivateMode)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"Error: /openurl may only be called with --incognito or "
|
||||
"--no-incognito, not both at the same time."));
|
||||
return "";
|
||||
}
|
||||
|
||||
bool usePrivateMode = false;
|
||||
|
||||
if (forceNonPrivateMode)
|
||||
{
|
||||
usePrivateMode = false;
|
||||
}
|
||||
else if (supportsIncognitoLinks() &&
|
||||
(forcePrivateMode || preferPrivateMode))
|
||||
{
|
||||
usePrivateMode = true;
|
||||
}
|
||||
|
||||
bool res = false;
|
||||
if (supportsIncognitoLinks() && getSettings()->openLinksIncognito)
|
||||
if (usePrivateMode)
|
||||
{
|
||||
res = openLinkIncognito(url.toString(QUrl::FullyEncoded));
|
||||
}
|
||||
|
|
|
@ -59,6 +59,26 @@ void UserSource::initializeFromChannel(const Channel *channel)
|
|||
}
|
||||
|
||||
this->items_ = tc->accessChatters()->all();
|
||||
|
||||
if (getSettings()->alwaysIncludeBroadcasterInUserCompletions)
|
||||
{
|
||||
auto it = std::find_if(this->items_.begin(), this->items_.end(),
|
||||
[tc](const UserItem &user) {
|
||||
return user.first == tc->getName();
|
||||
});
|
||||
|
||||
if (it != this->items_.end())
|
||||
{
|
||||
auto broadcaster = *it;
|
||||
this->items_.erase(it);
|
||||
this->items_.insert(this->items_.begin(), broadcaster);
|
||||
}
|
||||
else
|
||||
{
|
||||
this->items_.insert(this->items_.begin(),
|
||||
{tc->getName(), tc->getDisplayName()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<UserItem> &UserSource::output() const
|
||||
|
|
|
@ -227,7 +227,8 @@ void NotificationController::removeFakeChannel(const QString channelName)
|
|||
{
|
||||
const auto &s = snapshot[i];
|
||||
|
||||
if (s->messageText == liveMessageSearchText)
|
||||
if (QString::compare(s->messageText, liveMessageSearchText,
|
||||
Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
s->flags.set(MessageFlag::Disabled);
|
||||
break;
|
||||
|
|
|
@ -126,97 +126,6 @@ int c2_register_callback(lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int c2_send_msg(lua_State *L)
|
||||
{
|
||||
QString text;
|
||||
QString channel;
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L, "send_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L, "cannot get text (2nd argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get channel (1st argument of send_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to send a message (using send_msg) to channel" << channel
|
||||
<< "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString message = text;
|
||||
message = message.replace('\n', ' ');
|
||||
QString outText =
|
||||
getIApp()->getCommands()->execCommand(message, chn, false);
|
||||
chn->sendMessage(outText);
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_system_msg(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L,
|
||||
"system_msg needs exactly 2 arguments (channel and text)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
QString channel;
|
||||
QString text;
|
||||
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L,
|
||||
"cannot get text (2nd argument of system_msg, expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
if (!lua::pop(L, &channel))
|
||||
{
|
||||
luaL_error(L, "cannot get channel (1st argument of system_msg, "
|
||||
"expected a string)");
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const auto chn = getApp()->twitch->getChannelOrEmpty(channel);
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
qCWarning(chatterinoLua)
|
||||
<< "Plugin" << pl->id
|
||||
<< "tried to show a system message (using system_msg) in channel"
|
||||
<< channel << "which is not known";
|
||||
lua::push(L, false);
|
||||
return 1;
|
||||
}
|
||||
chn->addMessage(makeSystemMessage(text));
|
||||
lua::push(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int c2_log(lua_State *L)
|
||||
{
|
||||
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
|
@ -238,6 +147,63 @@ int c2_log(lua_State *L)
|
|||
return 0;
|
||||
}
|
||||
|
||||
int c2_later(lua_State *L)
|
||||
{
|
||||
auto *pl = getIApp()->getPlugins()->getPluginByStatePtr(L);
|
||||
if (pl == nullptr)
|
||||
{
|
||||
return luaL_error(L, "c2.later: internal error: no plugin?");
|
||||
}
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
return luaL_error(
|
||||
L, "c2.later expects two arguments (a callback that takes no "
|
||||
"arguments and returns nothing and a number the time in "
|
||||
"milliseconds to wait)\n");
|
||||
}
|
||||
int time{};
|
||||
if (!lua::pop(L, &time))
|
||||
{
|
||||
return luaL_error(L, "cannot get time (2nd arg of c2.later, "
|
||||
"expected a number)");
|
||||
}
|
||||
|
||||
if (!lua_isfunction(L, lua_gettop(L)))
|
||||
{
|
||||
return luaL_error(L, "cannot get callback (1st arg of c2.later, "
|
||||
"expected a function)");
|
||||
}
|
||||
|
||||
auto *timer = new QTimer();
|
||||
timer->setInterval(time);
|
||||
auto id = pl->addTimeout(timer);
|
||||
auto name = QString("timeout_%1").arg(id);
|
||||
auto *coro = lua_newthread(L);
|
||||
|
||||
QObject::connect(timer, &QTimer::timeout, [pl, coro, name, timer]() {
|
||||
timer->deleteLater();
|
||||
pl->removeTimeout(timer);
|
||||
int nres{};
|
||||
lua_resume(coro, nullptr, 0, &nres);
|
||||
|
||||
lua_pushnil(coro);
|
||||
lua_setfield(coro, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
||||
if (lua_gettop(coro) != 0)
|
||||
{
|
||||
stackDump(coro,
|
||||
pl->id +
|
||||
": timer returned a value, this shouldn't happen "
|
||||
"and is probably a plugin bug");
|
||||
}
|
||||
});
|
||||
stackDump(L, "before setfield");
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, name.toStdString().c_str());
|
||||
lua_xmove(L, coro, 1); // move function to thread
|
||||
timer->start();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int g_load(lua_State *L)
|
||||
{
|
||||
# ifdef NDEBUG
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include <lua.h>
|
||||
# include <QString>
|
||||
|
||||
# include <cassert>
|
||||
# include <memory>
|
||||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
|
@ -31,7 +35,7 @@ enum class EventType {
|
|||
/**
|
||||
* @lua@class CommandContext
|
||||
* @lua@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
|
||||
* @lua@field channel_name string The name of the channel the command was executed in.
|
||||
* @lua@field channel Channel The channel the command was executed in.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -49,6 +53,11 @@ struct CompletionList {
|
|||
bool hideOthers{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @includefile common/Channel.hpp
|
||||
* @includefile controllers/plugins/api/ChannelRef.hpp
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registers a new command called `name` which when executed will call `handler`.
|
||||
*
|
||||
|
@ -68,27 +77,6 @@ int c2_register_command(lua_State *L);
|
|||
*/
|
||||
int c2_register_callback(lua_State *L);
|
||||
|
||||
/**
|
||||
* Sends a message to `channel` with the specified text. Also executes commands.
|
||||
*
|
||||
* **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
|
||||
*
|
||||
* @lua@param channel string The name of the Twitch channel
|
||||
* @lua@param text string The text to be sent
|
||||
* @lua@return boolean ok
|
||||
* @exposed c2.send_msg
|
||||
*/
|
||||
int c2_send_msg(lua_State *L);
|
||||
/**
|
||||
* Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
|
||||
*
|
||||
* @lua@param channel string
|
||||
* @lua@param text string
|
||||
* @lua@return boolean ok
|
||||
* @exposed c2.system_msg
|
||||
*/
|
||||
int c2_system_msg(lua_State *L);
|
||||
|
||||
/**
|
||||
* Writes a message to the Chatterino log.
|
||||
*
|
||||
|
@ -98,6 +86,15 @@ int c2_system_msg(lua_State *L);
|
|||
*/
|
||||
int c2_log(lua_State *L);
|
||||
|
||||
/**
|
||||
* Calls callback around msec milliseconds later. Does not freeze Chatterino.
|
||||
*
|
||||
* @lua@param callback fun() The callback that will be called.
|
||||
* @lua@param msec number How long to wait.
|
||||
* @exposed c2.later
|
||||
*/
|
||||
int c2_later(lua_State *L);
|
||||
|
||||
// These ones are global
|
||||
int g_load(lua_State *L);
|
||||
int g_print(lua_State *L);
|
||||
|
@ -107,6 +104,115 @@ int g_print(lua_State *L);
|
|||
int searcherAbsolute(lua_State *L);
|
||||
int searcherRelative(lua_State *L);
|
||||
|
||||
// This is a fat pointer that allows us to type check values given to functions needing a userdata.
|
||||
// Ensure ALL userdata given to Lua are a subclass of this! Otherwise we garbage as a pointer!
|
||||
struct UserData {
|
||||
enum class Type { Channel };
|
||||
Type type;
|
||||
bool isWeak;
|
||||
};
|
||||
|
||||
template <UserData::Type T, typename U>
|
||||
struct WeakPtrUserData : public UserData {
|
||||
std::weak_ptr<U> target;
|
||||
|
||||
WeakPtrUserData(std::weak_ptr<U> t)
|
||||
: UserData()
|
||||
, target(t)
|
||||
{
|
||||
this->type = T;
|
||||
this->isWeak = true;
|
||||
}
|
||||
|
||||
static WeakPtrUserData<T, U> *create(lua_State *L, std::weak_ptr<U> target)
|
||||
{
|
||||
void *ptr = lua_newuserdata(L, sizeof(WeakPtrUserData<T, U>));
|
||||
return new (ptr) WeakPtrUserData<T, U>(target);
|
||||
}
|
||||
|
||||
static WeakPtrUserData<T, U> *from(UserData *target)
|
||||
{
|
||||
if (!target->isWeak)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (target->type != T)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<WeakPtrUserData<T, U> *>(target);
|
||||
}
|
||||
|
||||
static WeakPtrUserData<T, U> *from(void *target)
|
||||
{
|
||||
return from(reinterpret_cast<UserData *>(target));
|
||||
}
|
||||
|
||||
static int destroy(lua_State *L)
|
||||
{
|
||||
auto self = WeakPtrUserData<T, U>::from(lua_touserdata(L, -1));
|
||||
// Note it is safe to only check the weakness of the pointer, as
|
||||
// std::weak_ptr seems to have identical representation regardless of
|
||||
// what it points to
|
||||
assert(self->isWeak);
|
||||
|
||||
self->target.reset();
|
||||
lua_pop(L, 1); // Lua deallocates the memory for full user data
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
template <UserData::Type T, typename U>
|
||||
struct SharedPtrUserData : public UserData {
|
||||
std::shared_ptr<U> target;
|
||||
|
||||
SharedPtrUserData(std::shared_ptr<U> t)
|
||||
: UserData()
|
||||
, target(t)
|
||||
{
|
||||
this->type = T;
|
||||
this->isWeak = false;
|
||||
}
|
||||
|
||||
static SharedPtrUserData<T, U> *create(lua_State *L,
|
||||
std::shared_ptr<U> target)
|
||||
{
|
||||
void *ptr = lua_newuserdata(L, sizeof(SharedPtrUserData<T, U>));
|
||||
return new (ptr) SharedPtrUserData<T, U>(target);
|
||||
}
|
||||
|
||||
static SharedPtrUserData<T, U> *from(UserData *target)
|
||||
{
|
||||
if (target->isWeak)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (target->type != T)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<SharedPtrUserData<T, U> *>(target);
|
||||
}
|
||||
|
||||
static SharedPtrUserData<T, U> *from(void *target)
|
||||
{
|
||||
return from(reinterpret_cast<UserData *>(target));
|
||||
}
|
||||
|
||||
static int destroy(lua_State *L)
|
||||
{
|
||||
auto self = SharedPtrUserData<T, U>::from(lua_touserdata(L, -1));
|
||||
// Note it is safe to only check the weakness of the pointer, as
|
||||
// std::shared_ptr seems to have identical representation regardless of
|
||||
// what it points to
|
||||
assert(!self->isWeak);
|
||||
|
||||
self->target.reset();
|
||||
lua_pop(L, 1); // Lua deallocates the memory for full user data
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
# include "common/Channel.hpp"
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
|
@ -120,8 +121,9 @@ StackIdx push(lua_State *L, const CommandContext &ctx)
|
|||
|
||||
push(L, ctx.words);
|
||||
lua_setfield(L, outIdx, "words");
|
||||
push(L, ctx.channel->getName());
|
||||
lua_setfield(L, outIdx, "channel_name");
|
||||
|
||||
push(L, ctx.channel);
|
||||
lua_setfield(L, outIdx, "channel");
|
||||
|
||||
return outIdx;
|
||||
}
|
||||
|
@ -138,6 +140,18 @@ StackIdx push(lua_State *L, const int &b)
|
|||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, int *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
if (lua_isnumber(L, idx) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
*out = lua_tointeger(L, idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek(lua_State *L, bool *out, StackIdx idx)
|
||||
{
|
||||
StackGuard guard(L);
|
||||
|
|
|
@ -66,6 +66,7 @@ StackIdx push(lua_State *L, const bool &b);
|
|||
StackIdx push(lua_State *L, const int &b);
|
||||
|
||||
// returns OK?
|
||||
bool peek(lua_State *L, int *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, bool *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, double *out, StackIdx idx = -1);
|
||||
bool peek(lua_State *L, QString *out, StackIdx idx = -1);
|
||||
|
@ -137,6 +138,17 @@ public:
|
|||
|
||||
/// TEMPLATES
|
||||
|
||||
template <typename T>
|
||||
StackIdx push(lua_State *L, std::optional<T> val)
|
||||
{
|
||||
if (val.has_value())
|
||||
{
|
||||
return lua::push(L, *val);
|
||||
}
|
||||
lua_pushnil(L);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool peek(lua_State *L, std::optional<T> *out, StackIdx idx = -1)
|
||||
{
|
||||
|
@ -262,7 +274,7 @@ StackIdx push(lua_State *L, QList<T> vec)
|
|||
*
|
||||
* @return Stack index of newly created string.
|
||||
*/
|
||||
template <typename T, std::enable_if<std::is_enum_v<T>>>
|
||||
template <typename T, typename std::enable_if_t<std::is_enum_v<T>, bool> = true>
|
||||
StackIdx push(lua_State *L, T inp)
|
||||
{
|
||||
std::string_view name = magic_enum::enum_name<T>(inp);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/Plugin.hpp"
|
||||
|
||||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
|
||||
# include <lua.h>
|
||||
|
@ -167,11 +168,38 @@ std::unordered_set<QString> Plugin::listRegisteredCommands()
|
|||
|
||||
Plugin::~Plugin()
|
||||
{
|
||||
for (auto *timer : this->activeTimeouts)
|
||||
{
|
||||
QObject::disconnect(timer, nullptr, nullptr, nullptr);
|
||||
timer->deleteLater();
|
||||
}
|
||||
qCDebug(chatterinoLua) << "Destroyed" << this->activeTimeouts.size()
|
||||
<< "timers for plugin" << this->id
|
||||
<< "while destroying the object";
|
||||
this->activeTimeouts.clear();
|
||||
if (this->state_ != nullptr)
|
||||
{
|
||||
lua_close(this->state_);
|
||||
}
|
||||
}
|
||||
int Plugin::addTimeout(QTimer *timer)
|
||||
{
|
||||
this->activeTimeouts.push_back(timer);
|
||||
return ++this->lastTimerId;
|
||||
}
|
||||
|
||||
void Plugin::removeTimeout(QTimer *timer)
|
||||
{
|
||||
for (auto it = this->activeTimeouts.begin();
|
||||
it != this->activeTimeouts.end(); ++it)
|
||||
{
|
||||
if (*it == timer)
|
||||
{
|
||||
this->activeTimeouts.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#endif
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# include <vector>
|
||||
|
||||
struct lua_State;
|
||||
class QTimer;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -126,6 +127,9 @@ public:
|
|||
return this->error_;
|
||||
}
|
||||
|
||||
int addTimeout(QTimer *timer);
|
||||
void removeTimeout(QTimer *timer);
|
||||
|
||||
private:
|
||||
QDir loadDirectory_;
|
||||
lua_State *state_;
|
||||
|
@ -134,6 +138,8 @@ private:
|
|||
|
||||
// maps command name -> function name
|
||||
std::unordered_map<QString, QString> ownedCommands;
|
||||
std::vector<QTimer *> activeTimeouts;
|
||||
int lastTimerId = 0;
|
||||
|
||||
friend class PluginController;
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
# include "common/QLogging.hpp"
|
||||
# include "controllers/commands/CommandContext.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
|
@ -117,8 +118,7 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||
luaL_Reg{LUA_GNAME, luaopen_base},
|
||||
// - load - don't allow in release mode
|
||||
|
||||
//luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||
// - needs special support
|
||||
luaL_Reg{LUA_COLIBNAME, luaopen_coroutine},
|
||||
luaL_Reg{LUA_TABLIBNAME, luaopen_table},
|
||||
// luaL_Reg{LUA_IOLIBNAME, luaopen_io},
|
||||
// - explicit fs access, needs wrapper with permissions, no usage ideas yet
|
||||
|
@ -143,11 +143,10 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg c2Lib[] = {
|
||||
{"system_msg", lua::api::c2_system_msg},
|
||||
{"register_command", lua::api::c2_register_command},
|
||||
{"register_callback", lua::api::c2_register_callback},
|
||||
{"send_msg", lua::api::c2_send_msg},
|
||||
{"log", lua::api::c2_log},
|
||||
{"later", lua::api::c2_later},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
lua_pushglobaltable(L);
|
||||
|
@ -164,6 +163,16 @@ void PluginController::openLibrariesFor(lua_State *L, const PluginMeta &meta,
|
|||
lua::pushEnumTable<lua::api::EventType>(L);
|
||||
lua_setfield(L, c2libIdx, "EventType");
|
||||
|
||||
lua::pushEnumTable<lua::api::LPlatform>(L);
|
||||
lua_setfield(L, c2libIdx, "Platform");
|
||||
|
||||
lua::pushEnumTable<Channel::Type>(L);
|
||||
lua_setfield(L, c2libIdx, "ChannelType");
|
||||
|
||||
// Initialize metatables for objects
|
||||
lua::api::ChannelRef::createMetatable(L);
|
||||
lua_setfield(L, c2libIdx, "Channel");
|
||||
|
||||
lua_setfield(L, gtable, "c2");
|
||||
|
||||
// ban functions
|
||||
|
@ -330,6 +339,11 @@ bool PluginController::isPluginEnabled(const QString &id)
|
|||
|
||||
Plugin *PluginController::getPluginByStatePtr(lua_State *L)
|
||||
{
|
||||
lua_geti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
|
||||
// Use the main thread for identification, not a coroutine instance
|
||||
auto *mainL = lua_tothread(L, -1);
|
||||
lua_pop(L, 1);
|
||||
L = mainL;
|
||||
for (auto &[name, plugin] : this->plugins_)
|
||||
{
|
||||
if (plugin->state_ == L)
|
||||
|
|
393
src/controllers/plugins/api/ChannelRef.cpp
Normal file
393
src/controllers/plugins/api/ChannelRef.cpp
Normal file
|
@ -0,0 +1,393 @@
|
|||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/LuaAPI.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "messages/MessageBuilder.hpp"
|
||||
# include "providers/twitch/TwitchChannel.hpp"
|
||||
# include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
# include <lauxlib.h>
|
||||
# include <lua.h>
|
||||
|
||||
# include <cassert>
|
||||
# include <memory>
|
||||
# include <optional>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(*vararg)
|
||||
|
||||
// NOLINTNEXTLINE(*-avoid-c-arrays)
|
||||
static const luaL_Reg CHANNEL_REF_METHODS[] = {
|
||||
{"is_valid", &ChannelRef::is_valid},
|
||||
{"get_name", &ChannelRef::get_name},
|
||||
{"get_type", &ChannelRef::get_type},
|
||||
{"get_display_name", &ChannelRef::get_display_name},
|
||||
{"send_message", &ChannelRef::send_message},
|
||||
{"add_system_message", &ChannelRef::add_system_message},
|
||||
{"is_twitch_channel", &ChannelRef::is_twitch_channel},
|
||||
|
||||
// Twitch
|
||||
{"get_room_modes", &ChannelRef::get_room_modes},
|
||||
{"get_stream_status", &ChannelRef::get_stream_status},
|
||||
{"get_twitch_id", &ChannelRef::get_twitch_id},
|
||||
{"is_broadcaster", &ChannelRef::is_broadcaster},
|
||||
{"is_mod", &ChannelRef::is_mod},
|
||||
{"is_vip", &ChannelRef::is_vip},
|
||||
|
||||
// misc
|
||||
{"__tostring", &ChannelRef::to_string},
|
||||
|
||||
// static
|
||||
{"by_name", &ChannelRef::get_by_name},
|
||||
{"by_twitch_id", &ChannelRef::get_by_twitch_id},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
void ChannelRef::createMetatable(lua_State *L)
|
||||
{
|
||||
lua::StackGuard guard(L, 1);
|
||||
|
||||
luaL_newmetatable(L, "c2.Channel");
|
||||
lua_pushstring(L, "__index");
|
||||
lua_pushvalue(L, -2); // clone metatable
|
||||
lua_settable(L, -3); // metatable.__index = metatable
|
||||
|
||||
// Generic IWeakResource stuff
|
||||
lua_pushstring(L, "__gc");
|
||||
lua_pushcfunction(
|
||||
L, (&WeakPtrUserData<UserData::Type::Channel, ChannelRef>::destroy));
|
||||
lua_settable(L, -3); // metatable.__gc = WeakPtrUserData<...>::destroy
|
||||
|
||||
luaL_setfuncs(L, CHANNEL_REF_METHODS, 0);
|
||||
}
|
||||
|
||||
ChannelPtr ChannelRef::getOrError(lua_State *L, bool expiredOk)
|
||||
{
|
||||
if (lua_gettop(L) < 1)
|
||||
{
|
||||
luaL_error(L, "Called c2.Channel method without a channel object");
|
||||
return nullptr;
|
||||
}
|
||||
if (lua_isuserdata(L, lua_gettop(L)) == 0)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Called c2.Channel method with a non Channel 'self' argument.");
|
||||
return nullptr;
|
||||
}
|
||||
auto *data = WeakPtrUserData<UserData::Type::Channel, Channel>::from(
|
||||
lua_touserdata(L, lua_gettop(L)));
|
||||
if (data == nullptr)
|
||||
{
|
||||
luaL_error(L,
|
||||
"Called c2.Channel method with an invalid channel pointer");
|
||||
return nullptr;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (data->target.expired())
|
||||
{
|
||||
if (!expiredOk)
|
||||
{
|
||||
luaL_error(L,
|
||||
"Usage of expired c2.Channel object. Underlying "
|
||||
"resource was freed. Use Channel:is_valid() to check");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return data->target.lock();
|
||||
}
|
||||
|
||||
std::shared_ptr<TwitchChannel> ChannelRef::getTwitchOrError(lua_State *L)
|
||||
{
|
||||
auto ref = ChannelRef::getOrError(L);
|
||||
auto ptr = dynamic_pointer_cast<TwitchChannel>(ref);
|
||||
if (ptr == nullptr)
|
||||
{
|
||||
luaL_error(L,
|
||||
"c2.Channel Twitch-only operation on non-Twitch channel.");
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
int ChannelRef::is_valid(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||
lua::push(L, that != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_name(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getName());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_type(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getType());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_display_name(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->getDisplayName());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::send_message(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2 && lua_gettop(L) != 3)
|
||||
{
|
||||
luaL_error(L, "Channel:send_message needs 1 or 2 arguments (message "
|
||||
"text and optionally execute_commands flag)");
|
||||
return 0;
|
||||
}
|
||||
bool execcmds = false;
|
||||
if (lua_gettop(L) == 3)
|
||||
{
|
||||
if (!lua::pop(L, &execcmds))
|
||||
{
|
||||
luaL_error(L, "cannot get execute_commands (2nd argument of "
|
||||
"Channel:send_message)");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
QString text;
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(L, "cannot get text (1st argument of Channel:send_message)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
|
||||
text = text.replace('\n', ' ');
|
||||
if (execcmds)
|
||||
{
|
||||
text = getIApp()->getCommands()->execCommand(text, that, false);
|
||||
}
|
||||
that->sendMessage(text);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ChannelRef::add_system_message(lua_State *L)
|
||||
{
|
||||
// needs to account for the hidden self argument
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Channel:add_system_message needs exactly 1 argument (message "
|
||||
"text)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
QString text;
|
||||
if (!lua::pop(L, &text))
|
||||
{
|
||||
luaL_error(
|
||||
L, "cannot get text (1st argument of Channel:add_system_message)");
|
||||
return 0;
|
||||
}
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
text = text.replace('\n', ' ');
|
||||
that->addMessage(makeSystemMessage(text));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ChannelRef::is_twitch_channel(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L);
|
||||
lua::push(L, that->isTwitchChannel());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_room_modes(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
const auto m = tc->accessRoomModes();
|
||||
const auto modes = LuaRoomModes{
|
||||
.unique_chat = m->r9k,
|
||||
.subscriber_only = m->submode,
|
||||
.emotes_only = m->emoteOnly,
|
||||
.follower_only = (m->followerOnly == -1)
|
||||
? std::nullopt
|
||||
: std::optional(m->followerOnly),
|
||||
.slow_mode =
|
||||
(m->slowMode == 0) ? std::nullopt : std::optional(m->slowMode),
|
||||
|
||||
};
|
||||
lua::push(L, modes);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_stream_status(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
const auto s = tc->accessStreamStatus();
|
||||
const auto status = LuaStreamStatus{
|
||||
.live = s->live,
|
||||
.viewer_count = static_cast<int>(s->viewerCount),
|
||||
.uptime = s->uptimeSeconds,
|
||||
.title = s->title,
|
||||
.game_name = s->game,
|
||||
.game_id = s->gameId,
|
||||
};
|
||||
lua::push(L, status);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_twitch_id(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->roomId());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_broadcaster(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isBroadcaster());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_mod(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isMod());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::is_vip(lua_State *L)
|
||||
{
|
||||
auto tc = ChannelRef::getTwitchOrError(L);
|
||||
lua::push(L, tc->isVip());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_by_name(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 2)
|
||||
{
|
||||
luaL_error(L, "Channel.by_name needs exactly 2 arguments (channel "
|
||||
"name and platform)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
LPlatform platform{};
|
||||
if (!lua::pop(L, &platform))
|
||||
{
|
||||
luaL_error(L, "cannot get platform (2nd argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
QString name;
|
||||
if (!lua::pop(L, &name))
|
||||
{
|
||||
luaL_error(L,
|
||||
"cannot get channel name (1st argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
auto chn = getApp()->twitch->getChannelOrEmpty(name);
|
||||
lua::push(L, chn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::get_by_twitch_id(lua_State *L)
|
||||
{
|
||||
if (lua_gettop(L) != 1)
|
||||
{
|
||||
luaL_error(
|
||||
L, "Channel.by_twitch_id needs exactly 1 arguments (channel owner "
|
||||
"id)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
QString id;
|
||||
if (!lua::pop(L, &id))
|
||||
{
|
||||
luaL_error(L,
|
||||
"cannot get channel name (1st argument of Channel.by_name, "
|
||||
"expected a string)");
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
auto chn = getApp()->twitch->getChannelOrEmptyByID(id);
|
||||
|
||||
lua::push(L, chn);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ChannelRef::to_string(lua_State *L)
|
||||
{
|
||||
ChannelPtr that = ChannelRef::getOrError(L, true);
|
||||
if (that == nullptr)
|
||||
{
|
||||
lua_pushstring(L, "<c2.Channel expired>");
|
||||
return 1;
|
||||
}
|
||||
QString formated = QString("<c2.Channel %1>").arg(that->getName());
|
||||
lua::push(L, formated);
|
||||
return 1;
|
||||
}
|
||||
} // namespace chatterino::lua::api
|
||||
// NOLINTEND(*vararg)
|
||||
//
|
||||
namespace chatterino::lua {
|
||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes)
|
||||
{
|
||||
auto out = lua::pushEmptyTable(L, 6);
|
||||
# define PUSH(field) \
|
||||
lua::push(L, modes.field); \
|
||||
lua_setfield(L, out, #field)
|
||||
PUSH(unique_chat);
|
||||
PUSH(subscriber_only);
|
||||
PUSH(emotes_only);
|
||||
PUSH(follower_only);
|
||||
PUSH(slow_mode);
|
||||
# undef PUSH
|
||||
return out;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status)
|
||||
{
|
||||
auto out = lua::pushEmptyTable(L, 6);
|
||||
# define PUSH(field) \
|
||||
lua::push(L, status.field); \
|
||||
lua_setfield(L, out, #field)
|
||||
PUSH(live);
|
||||
PUSH(viewer_count);
|
||||
PUSH(uptime);
|
||||
PUSH(title);
|
||||
PUSH(game_name);
|
||||
PUSH(game_id);
|
||||
# undef PUSH
|
||||
return out;
|
||||
}
|
||||
|
||||
StackIdx push(lua_State *L, ChannelPtr chn)
|
||||
{
|
||||
using namespace chatterino::lua::api;
|
||||
|
||||
if (chn->isEmpty())
|
||||
{
|
||||
lua_pushnil(L);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
WeakPtrUserData<UserData::Type::Channel, Channel>::create(
|
||||
L, chn->weak_from_this());
|
||||
luaL_getmetatable(L, "c2.Channel");
|
||||
lua_setmetatable(L, -2);
|
||||
return lua_gettop(L);
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
276
src/controllers/plugins/api/ChannelRef.hpp
Normal file
276
src/controllers/plugins/api/ChannelRef.hpp
Normal file
|
@ -0,0 +1,276 @@
|
|||
#pragma once
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
|
||||
#include <optional>
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include "common/Channel.hpp"
|
||||
# include "controllers/plugins/LuaUtilities.hpp"
|
||||
# include "controllers/plugins/PluginController.hpp"
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
|
||||
/**
|
||||
* This enum describes a platform for the purpose of searching for a channel.
|
||||
* Currently only Twitch is supported because identifying IRC channels is tricky.
|
||||
* @exposeenum Platform
|
||||
*/
|
||||
enum class LPlatform {
|
||||
Twitch,
|
||||
//IRC,
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class Channel: IWeakResource
|
||||
*/
|
||||
struct ChannelRef {
|
||||
static void createMetatable(lua_State *L);
|
||||
friend class chatterino::PluginController;
|
||||
|
||||
/**
|
||||
* @brief Get the content of the top object on Lua stack, usually first argument to function as a ChannelPtr.
|
||||
* If the object given is not a userdatum or the pointer inside that
|
||||
* userdatum doesn't point to a Channel, a lua error is thrown.
|
||||
*
|
||||
* @param expiredOk Should an expired return nullptr instead of erroring
|
||||
*/
|
||||
static ChannelPtr getOrError(lua_State *L, bool expiredOk = false);
|
||||
|
||||
/**
|
||||
* @brief Casts the result of getOrError to std::shared_ptr<TwitchChannel>
|
||||
* if that fails thows a lua error.
|
||||
*/
|
||||
static std::shared_ptr<TwitchChannel> getTwitchOrError(lua_State *L);
|
||||
|
||||
public:
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @lua@return boolean success
|
||||
* @exposed Channel:is_valid
|
||||
*/
|
||||
static int is_valid(lua_State *L);
|
||||
|
||||
/**
|
||||
* Gets the channel's name. This is the lowercase login name.
|
||||
*
|
||||
* @lua@return string name
|
||||
* @exposed Channel:get_name
|
||||
*/
|
||||
static int get_name(lua_State *L);
|
||||
|
||||
/**
|
||||
* Gets the channel's type
|
||||
*
|
||||
* @lua@return ChannelType
|
||||
* @exposed Channel:get_type
|
||||
*/
|
||||
static int get_type(lua_State *L);
|
||||
|
||||
/**
|
||||
* Get the channel owner's display name. This may contain non-lowercase ascii characters.
|
||||
*
|
||||
* @lua@return string name
|
||||
* @exposed Channel:get_display_name
|
||||
*/
|
||||
static int get_display_name(lua_State *L);
|
||||
|
||||
/**
|
||||
* Sends a message to the target channel.
|
||||
* Note that this does not execute client-commands.
|
||||
*
|
||||
* @lua@param message string
|
||||
* @lua@param execute_commands boolean Should commands be run on the text?
|
||||
* @exposed Channel:send_message
|
||||
*/
|
||||
static int send_message(lua_State *L);
|
||||
|
||||
/**
|
||||
* Adds a system message client-side
|
||||
*
|
||||
* @lua@param message string
|
||||
* @exposed Channel:add_system_message
|
||||
*/
|
||||
static int add_system_message(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns true for twitch channels.
|
||||
* Compares the channel Type. Note that enum values aren't guaranteed, just
|
||||
* that they are equal to the exposed enum.
|
||||
*
|
||||
* @lua@return bool
|
||||
* @exposed Channel:is_twitch_channel
|
||||
*/
|
||||
static int is_twitch_channel(lua_State *L);
|
||||
|
||||
/**
|
||||
* Twitch Channel specific functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a copy of the channel mode settings (subscriber only, r9k etc.)
|
||||
*
|
||||
* @lua@return RoomModes
|
||||
* @exposed Channel:get_room_modes
|
||||
*/
|
||||
static int get_room_modes(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns a copy of the stream status.
|
||||
*
|
||||
* @lua@return StreamStatus
|
||||
* @exposed Channel:get_stream_status
|
||||
*/
|
||||
static int get_stream_status(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns the Twitch user ID of the owner of the channel.
|
||||
*
|
||||
* @lua@return string
|
||||
* @exposed Channel:get_twitch_id
|
||||
*/
|
||||
static int get_twitch_id(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user owns it
|
||||
*
|
||||
* @lua@return boolean
|
||||
* @exposed Channel:is_broadcaster
|
||||
*/
|
||||
static int is_broadcaster(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user is a moderator in the channel
|
||||
* Returns false for broadcaster.
|
||||
*
|
||||
* @lua@return boolean
|
||||
* @exposed Channel:is_mod
|
||||
*/
|
||||
static int is_mod(lua_State *L);
|
||||
|
||||
/**
|
||||
* Returns true if the channel is a Twitch channel and the user is a VIP in the channel
|
||||
* Returns false for broadcaster.
|
||||
*
|
||||
* @lua@return boolean
|
||||
* @exposed Channel:is_vip
|
||||
*/
|
||||
static int is_vip(lua_State *L);
|
||||
|
||||
/**
|
||||
* Misc
|
||||
*/
|
||||
|
||||
/**
|
||||
* @lua@return string
|
||||
* @exposed Channel:__tostring
|
||||
*/
|
||||
static int to_string(lua_State *L);
|
||||
|
||||
/**
|
||||
* Static functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Finds a channel by name.
|
||||
*
|
||||
* Misc channels are marked as Twitch:
|
||||
* - /whispers
|
||||
* - /mentions
|
||||
* - /watching
|
||||
* - /live
|
||||
* - /automod
|
||||
*
|
||||
* @lua@param name string Which channel are you looking for?
|
||||
* @lua@param platform Platform Where to search for the channel?
|
||||
* @lua@return Channel?
|
||||
* @exposed Channel.by_name
|
||||
*/
|
||||
static int get_by_name(lua_State *L);
|
||||
|
||||
/**
|
||||
* Finds a channel by the Twitch user ID of its owner.
|
||||
*
|
||||
* @lua@param string id ID of the owner of the channel.
|
||||
* @lua@return Channel?
|
||||
* @exposed Channel.by_twitch_id
|
||||
*/
|
||||
static int get_by_twitch_id(lua_State *L);
|
||||
};
|
||||
|
||||
// This is a copy of the TwitchChannel::RoomModes structure, except it uses nicer optionals
|
||||
/**
|
||||
* @lua@class RoomModes
|
||||
*/
|
||||
struct LuaRoomModes {
|
||||
/**
|
||||
* @lua@field unique_chat boolean You might know this as r9kbeta or robot9000.
|
||||
*/
|
||||
bool unique_chat = false;
|
||||
|
||||
/**
|
||||
* @lua@field subscriber_only boolean
|
||||
*/
|
||||
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
|
||||
*/
|
||||
bool emotes_only = false;
|
||||
|
||||
/**
|
||||
* @lua@field unique_chat number? Time in minutes you need to follow to chat or nil.
|
||||
*/
|
||||
std::optional<int> follower_only;
|
||||
/**
|
||||
* @lua@field slow_mode number? Time in seconds you need to wait before sending messages or nil.
|
||||
*/
|
||||
std::optional<int> slow_mode;
|
||||
};
|
||||
|
||||
/**
|
||||
* @lua@class StreamStatus
|
||||
*/
|
||||
struct LuaStreamStatus {
|
||||
/**
|
||||
* @lua@field live boolean
|
||||
*/
|
||||
bool live = false;
|
||||
|
||||
/**
|
||||
* @lua@field viewer_count number
|
||||
*/
|
||||
int viewer_count = 0;
|
||||
|
||||
/**
|
||||
* @lua@field uptime number Seconds since the stream started.
|
||||
*/
|
||||
int uptime = 0;
|
||||
|
||||
/**
|
||||
* @lua@field title string Stream title or last stream title
|
||||
*/
|
||||
QString title;
|
||||
|
||||
/**
|
||||
* @lua@field game_name string
|
||||
*/
|
||||
QString game_name;
|
||||
|
||||
/**
|
||||
* @lua@field game_id string
|
||||
*/
|
||||
QString game_id;
|
||||
};
|
||||
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
} // namespace chatterino::lua::api
|
||||
namespace chatterino::lua {
|
||||
StackIdx push(lua_State *L, const api::LuaRoomModes &modes);
|
||||
StackIdx push(lua_State *L, const api::LuaStreamStatus &status);
|
||||
StackIdx push(lua_State *L, ChannelPtr chn);
|
||||
} // namespace chatterino::lua
|
||||
#endif
|
|
@ -7,12 +7,12 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
static bool isGuiThread()
|
||||
inline bool isGuiThread()
|
||||
{
|
||||
return QCoreApplication::instance()->thread() == QThread::currentThread();
|
||||
}
|
||||
|
||||
static void assertInGuiThread()
|
||||
inline void assertInGuiThread()
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
assert(isGuiThread());
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/FlagsEnum.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <QColor>
|
||||
#include <QTime>
|
||||
|
||||
|
@ -57,6 +58,8 @@ enum class MessageFlag : int64_t {
|
|||
RestrictedMessage = (1LL << 33),
|
||||
/// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature
|
||||
MonitoredMessage = (1LL << 34),
|
||||
/// The message is an ACTION message (/me)
|
||||
Action = (1LL << 35),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
@ -105,3 +108,8 @@ struct Message {
|
|||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
template <>
|
||||
struct magic_enum::customize::enum_range<chatterino::MessageFlag> {
|
||||
static constexpr bool is_flags = true;
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
#include "providers/LinkResolver.hpp"
|
||||
#include "providers/links/LinkResolver.hpp"
|
||||
#include "providers/twitch/PubSubActions.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
|
@ -528,12 +528,12 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||
this->emplace<TimestampElement>();
|
||||
|
||||
using MEF = MessageElementFlag;
|
||||
auto addText = [this](QString text, MessageElementFlags mefs = MEF::Text,
|
||||
auto addText = [this](QString text,
|
||||
MessageColor color =
|
||||
MessageColor::System) -> TextElement * {
|
||||
this->message().searchText += text;
|
||||
this->message().messageText += text;
|
||||
return this->emplace<TextElement>(text, mefs, color);
|
||||
return this->emplace<TextElement>(text, MEF::Text, color);
|
||||
};
|
||||
|
||||
addText("Your image has been uploaded to");
|
||||
|
@ -541,16 +541,14 @@ MessageBuilder::MessageBuilder(ImageUploaderResultTag /*unused*/,
|
|||
// ASSUMPTION: the user gave this uploader configuration to the program
|
||||
// therefore they trust that the host is not wrong/malicious. This doesn't obey getSettings()->lowercaseDomains.
|
||||
// This also ensures that the LinkResolver doesn't get these links.
|
||||
addText(imageLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
||||
MessageColor::Link)
|
||||
addText(imageLink, MessageColor::Link)
|
||||
->setLink({Link::Url, imageLink})
|
||||
->setTrailingSpace(false);
|
||||
|
||||
if (!deletionLink.isEmpty())
|
||||
{
|
||||
addText("(Deletion link:");
|
||||
addText(deletionLink, {MEF::OriginalLink, MEF::LowercaseLink},
|
||||
MessageColor::Link)
|
||||
addText(deletionLink, MessageColor::Link)
|
||||
->setLink({Link::Url, deletionLink})
|
||||
->setTrailingSpace(false);
|
||||
addText(")")->setTrailingSpace(false);
|
||||
|
@ -634,46 +632,13 @@ void MessageBuilder::addLink(const ParsedLink &parsedLink)
|
|||
lowercaseLinkString += parsedLink.host.toString().toLower();
|
||||
lowercaseLinkString += parsedLink.rest;
|
||||
|
||||
auto linkElement = Link(Link::Url, matchedLink);
|
||||
|
||||
auto textColor = MessageColor(MessageColor::Link);
|
||||
auto *linkMELowercase =
|
||||
this->emplace<TextElement>(lowercaseLinkString,
|
||||
MessageElementFlag::LowercaseLink, textColor)
|
||||
->setLink(linkElement);
|
||||
auto *linkMEOriginal =
|
||||
this->emplace<TextElement>(origLink, MessageElementFlag::OriginalLink,
|
||||
textColor)
|
||||
->setLink(linkElement);
|
||||
|
||||
LinkResolver::getLinkInfo(
|
||||
matchedLink, nullptr,
|
||||
[weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal,
|
||||
matchedLink](QString tooltipText, Link originalLink,
|
||||
ImagePtr thumbnail) {
|
||||
auto shared = weakMessage.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!tooltipText.isEmpty())
|
||||
{
|
||||
linkMELowercase->setTooltip(tooltipText);
|
||||
linkMEOriginal->setTooltip(tooltipText);
|
||||
}
|
||||
if (originalLink.value != matchedLink &&
|
||||
!originalLink.value.isEmpty())
|
||||
{
|
||||
linkMELowercase->setLink(originalLink)->updateLink();
|
||||
linkMEOriginal->setLink(originalLink)->updateLink();
|
||||
}
|
||||
linkMELowercase->setThumbnail(thumbnail);
|
||||
linkMELowercase->setThumbnailType(
|
||||
MessageElement::ThumbnailType::Link_Thumbnail);
|
||||
linkMEOriginal->setThumbnail(thumbnail);
|
||||
linkMEOriginal->setThumbnailType(
|
||||
MessageElement::ThumbnailType::Link_Thumbnail);
|
||||
});
|
||||
auto *el = this->emplace<LinkElement>(
|
||||
LinkElement::Parsed{.lowercase = lowercaseLinkString,
|
||||
.original = matchedLink},
|
||||
MessageElementFlag::Text, textColor);
|
||||
el->setLink({Link::Url, matchedLink});
|
||||
getIApp()->getLinkResolver()->resolve(el->linkInfo());
|
||||
}
|
||||
|
||||
void MessageBuilder::addIrcMessageText(const QString &text)
|
||||
|
|
|
@ -63,18 +63,6 @@ MessageElement *MessageElement::setTooltip(const QString &tooltip)
|
|||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
|
||||
{
|
||||
this->thumbnail_ = thumbnail;
|
||||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
|
||||
{
|
||||
this->thumbnailType_ = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::setTrailingSpace(bool value)
|
||||
{
|
||||
this->trailingSpace = value;
|
||||
|
@ -86,17 +74,7 @@ const QString &MessageElement::getTooltip() const
|
|||
return this->tooltip_;
|
||||
}
|
||||
|
||||
const ImagePtr &MessageElement::getThumbnail() const
|
||||
{
|
||||
return this->thumbnail_;
|
||||
}
|
||||
|
||||
const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
|
||||
{
|
||||
return this->thumbnailType_;
|
||||
}
|
||||
|
||||
const Link &MessageElement::getLink() const
|
||||
Link MessageElement::getLink() const
|
||||
{
|
||||
return this->link_;
|
||||
}
|
||||
|
@ -116,12 +94,6 @@ void MessageElement::addFlags(MessageElementFlags flags)
|
|||
this->flags_.set(flags);
|
||||
}
|
||||
|
||||
MessageElement *MessageElement::updateLink()
|
||||
{
|
||||
this->linkChanged.invoke();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Empty
|
||||
EmptyElement::EmptyElement()
|
||||
: MessageElement(MessageElementFlag::None)
|
||||
|
@ -155,8 +127,8 @@ void ImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto size = QSize(this->image_->width() * container.getScale(),
|
||||
this->image_->height() * container.getScale());
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, this->image_, size))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(
|
||||
(new ImageLayoutElement(*this, this->image_, size)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,10 +150,8 @@ void CircularImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto imgSize = QSize(this->image_->width(), this->image_->height()) *
|
||||
container.getScale();
|
||||
|
||||
container.addElement((new ImageWithCircleBackgroundLayoutElement(
|
||||
*this, this->image_, imgSize,
|
||||
this->background_, this->padding_))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(new ImageWithCircleBackgroundLayoutElement(
|
||||
*this, this->image_, imgSize, this->background_, this->padding_));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,8 +192,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||
QSize(int(container.getScale() * image->width() * emoteScale),
|
||||
int(container.getScale() * image->height() * emoteScale));
|
||||
|
||||
container.addElement(this->makeImageLayoutElement(image, size)
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(this->makeImageLayoutElement(image, size));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -284,8 +253,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
|
||||
container.addElement(this->makeImageLayoutElement(
|
||||
images, individualSizes, largestSize)
|
||||
->setLink(this->getLink()));
|
||||
images, individualSizes, largestSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -441,8 +409,7 @@ EmotePtr BadgeElement::getEmote() const
|
|||
MessageLayoutElement *BadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
||||
auto *element = new ImageLayoutElement(*this, image, size);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -459,9 +426,8 @@ MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
|
|||
{
|
||||
static const QColor modBadgeBackgroundColor("#34AE0A");
|
||||
|
||||
auto *element = (new ImageWithBackgroundLayoutElement(
|
||||
*this, image, size, modBadgeBackgroundColor))
|
||||
->setLink(this->getLink());
|
||||
auto *element = new ImageWithBackgroundLayoutElement(
|
||||
*this, image, size, modBadgeBackgroundColor);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -476,8 +442,7 @@ VipBadgeElement::VipBadgeElement(const EmotePtr &data,
|
|||
MessageLayoutElement *VipBadgeElement::makeImageLayoutElement(
|
||||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
|
||||
auto *element = new ImageLayoutElement(*this, image, size);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -494,8 +459,7 @@ MessageLayoutElement *FfzBadgeElement::makeImageLayoutElement(
|
|||
const ImagePtr &image, const QSize &size)
|
||||
{
|
||||
auto *element =
|
||||
(new ImageWithBackgroundLayoutElement(*this, image, size, this->color))
|
||||
->setLink(this->getLink());
|
||||
new ImageWithBackgroundLayoutElement(*this, image, size, this->color);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
@ -507,11 +471,8 @@ TextElement::TextElement(const QString &text, MessageElementFlags flags,
|
|||
, color_(color)
|
||||
, style_(style)
|
||||
{
|
||||
for (const auto &word : text.split(' '))
|
||||
{
|
||||
this->words_.push_back({word, -1});
|
||||
// fourtf: add logic to store multiple spaces after message
|
||||
}
|
||||
this->words_ = text.split(' ');
|
||||
// fourtf: add logic to store multiple spaces after message
|
||||
}
|
||||
|
||||
void TextElement::addToContainer(MessageLayoutContainer &container,
|
||||
|
@ -524,39 +485,29 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
QFontMetrics metrics =
|
||||
app->getFonts()->getFontMetrics(this->style_, container.getScale());
|
||||
|
||||
for (Word &word : this->words_)
|
||||
for (const auto &word : this->words_)
|
||||
{
|
||||
auto getTextLayoutElement = [&](QString text, int width,
|
||||
bool hasTrailingSpace) {
|
||||
auto color = this->color_.getColor(*app->getThemes());
|
||||
app->getThemes()->normalizeColor(color);
|
||||
|
||||
auto *e = (new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()),
|
||||
color, this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
auto *e = new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale());
|
||||
e->setTrailingSpace(hasTrailingSpace);
|
||||
e->setText(text);
|
||||
|
||||
// If URL link was changed,
|
||||
// Should update it in MessageLayoutElement too!
|
||||
if (this->getLink().type == Link::Url)
|
||||
{
|
||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
// fourtf: add again
|
||||
// if (word.width == -1) {
|
||||
word.width = metrics.horizontalAdvance(word.text);
|
||||
// }
|
||||
auto width = metrics.horizontalAdvance(word);
|
||||
|
||||
// see if the text fits in the current line
|
||||
if (container.fitsInLine(word.width))
|
||||
if (container.fitsInLine(width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.width, this->hasTrailingSpace()));
|
||||
word, width, this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -565,35 +516,34 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
{
|
||||
container.breakLine();
|
||||
|
||||
if (container.fitsInLine(word.width))
|
||||
if (container.fitsInLine(width))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
word.text, word.width, this->hasTrailingSpace()));
|
||||
word, width, this->hasTrailingSpace()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// we done goofed, we need to wrap the text
|
||||
QString text = word.text;
|
||||
int textLength = text.length();
|
||||
auto textLength = word.length();
|
||||
int wordStart = 0;
|
||||
int width = 0;
|
||||
width = 0;
|
||||
|
||||
// QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
|
||||
|
||||
for (int i = 0; i < textLength; i++)
|
||||
{
|
||||
auto isSurrogate = text.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(text[i].unicode());
|
||||
auto isSurrogate = word.size() > i + 1 &&
|
||||
QChar::isHighSurrogate(word[i].unicode());
|
||||
|
||||
auto charWidth = isSurrogate
|
||||
? metrics.horizontalAdvance(text.mid(i, 2))
|
||||
: metrics.horizontalAdvance(text[i]);
|
||||
? metrics.horizontalAdvance(word.mid(i, 2))
|
||||
: metrics.horizontalAdvance(word[i]);
|
||||
|
||||
if (!container.fitsInLine(width + charWidth))
|
||||
{
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
text.mid(wordStart, i - wordStart), width, false));
|
||||
word.mid(wordStart, i - wordStart), width, false));
|
||||
container.breakLine();
|
||||
|
||||
wordStart = i;
|
||||
|
@ -615,7 +565,7 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
//add the final piece of wrapped text
|
||||
container.addElementNoLineBreak(getTextLayoutElement(
|
||||
text.mid(wordStart), width, this->hasTrailingSpace()));
|
||||
word.mid(wordStart), width, this->hasTrailingSpace()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -649,23 +599,16 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto color = this->color_.getColor(*app->getThemes());
|
||||
app->getThemes()->normalizeColor(color);
|
||||
|
||||
auto *e = (new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale()))
|
||||
->setLink(this->getLink());
|
||||
auto *e = new TextLayoutElement(
|
||||
*this, text, QSize(width, metrics.height()), color,
|
||||
this->style_, container.getScale());
|
||||
e->setTrailingSpace(hasTrailingSpace);
|
||||
e->setText(text);
|
||||
|
||||
// If URL link was changed,
|
||||
// Should update it in MessageLayoutElement too!
|
||||
if (this->getLink().type == Link::Url)
|
||||
{
|
||||
static_cast<TextLayoutElement *>(e)->listenToLinkChanges();
|
||||
}
|
||||
return e;
|
||||
};
|
||||
|
||||
static const auto ellipsis = QStringLiteral("...");
|
||||
static const auto ellipsis = QStringLiteral("…");
|
||||
|
||||
// String to continuously append words onto until we place it in the container
|
||||
// once we encounter an emote or reach the end of the message text. */
|
||||
|
@ -685,6 +628,7 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
currentText += ' ';
|
||||
}
|
||||
|
||||
bool done = false;
|
||||
for (const auto &parsedWord :
|
||||
app->getEmotes()->getEmojis()->parse(word.text))
|
||||
{
|
||||
|
@ -698,6 +642,7 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
container.remainingWidth());
|
||||
if (currentText != prev)
|
||||
{
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -720,6 +665,7 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
emoteSize.width()))
|
||||
{
|
||||
currentText += ellipsis;
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -735,6 +681,11 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last of the pending message text to the container.
|
||||
|
@ -749,6 +700,29 @@ void SingleLineTextElement::addToContainer(MessageLayoutContainer &container,
|
|||
}
|
||||
}
|
||||
|
||||
LinkElement::LinkElement(const Parsed &parsed, MessageElementFlags flags,
|
||||
const MessageColor &color, FontStyle style)
|
||||
: TextElement({}, flags, color, style)
|
||||
, linkInfo_(parsed.original)
|
||||
, lowercase_({parsed.lowercase})
|
||||
, original_({parsed.original})
|
||||
{
|
||||
this->setTooltip(parsed.original);
|
||||
}
|
||||
|
||||
void LinkElement::addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags)
|
||||
{
|
||||
this->words_ =
|
||||
getSettings()->lowercaseDomains ? this->lowercase_ : this->original_;
|
||||
TextElement::addToContainer(container, flags);
|
||||
}
|
||||
|
||||
Link LinkElement::getLink() const
|
||||
{
|
||||
return {Link::Url, this->linkInfo_.url()};
|
||||
}
|
||||
|
||||
// TIMESTAMP
|
||||
TimestampElement::TimestampElement(QTime time)
|
||||
: MessageElement(MessageElementFlag::Timestamp)
|
||||
|
@ -853,8 +827,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
|
|||
auto size = QSize(image->width() * container.getScale(),
|
||||
image->height() * container.getScale());
|
||||
|
||||
container.addElement((new ImageLayoutElement(*this, image, size))
|
||||
->setLink(this->getLink()));
|
||||
container.addElement(new ImageLayoutElement(*this, image, size));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "messages/ImageSet.hpp"
|
||||
#include "messages/Link.hpp"
|
||||
#include "messages/MessageColor.hpp"
|
||||
#include "providers/links/LinkInfo.hpp"
|
||||
#include "singletons/Fonts.hpp"
|
||||
|
||||
#include <pajlada/signals/signalholder.hpp>
|
||||
|
@ -136,10 +137,9 @@ enum class MessageElementFlag : int64_t {
|
|||
BoldUsername = (1LL << 27),
|
||||
NonBoldUsername = (1LL << 28),
|
||||
|
||||
// for links
|
||||
LowercaseLink = (1LL << 29),
|
||||
OriginalLink = (1LL << 30),
|
||||
|
||||
// used to check if links should be lowercased
|
||||
LowercaseLinks = (1LL << 29),
|
||||
// Unused = (1LL << 30)
|
||||
// Unused: (1LL << 31)
|
||||
|
||||
// for elements of the message reply
|
||||
|
@ -166,9 +166,6 @@ public:
|
|||
Update_Images = 4,
|
||||
Update_All = Update_Text | Update_Emotes | Update_Images
|
||||
};
|
||||
enum ThumbnailType : char {
|
||||
Link_Thumbnail = 1,
|
||||
};
|
||||
|
||||
virtual ~MessageElement();
|
||||
|
||||
|
@ -181,25 +178,18 @@ public:
|
|||
MessageElement *setLink(const Link &link);
|
||||
MessageElement *setText(const QString &text);
|
||||
MessageElement *setTooltip(const QString &tooltip);
|
||||
MessageElement *setThumbnailType(const ThumbnailType type);
|
||||
MessageElement *setThumbnail(const ImagePtr &thumbnail);
|
||||
|
||||
MessageElement *setTrailingSpace(bool value);
|
||||
const QString &getTooltip() const;
|
||||
const ImagePtr &getThumbnail() const;
|
||||
const ThumbnailType &getThumbnailType() const;
|
||||
|
||||
const Link &getLink() const;
|
||||
virtual Link getLink() const;
|
||||
bool hasTrailingSpace() const;
|
||||
MessageElementFlags getFlags() const;
|
||||
void addFlags(MessageElementFlags flags);
|
||||
MessageElement *updateLink();
|
||||
|
||||
virtual void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) = 0;
|
||||
|
||||
pajlada::Signals::NoArgSignal linkChanged;
|
||||
|
||||
protected:
|
||||
MessageElement(MessageElementFlags flags);
|
||||
bool trailingSpace = true;
|
||||
|
@ -208,8 +198,6 @@ private:
|
|||
QString text_;
|
||||
Link link_;
|
||||
QString tooltip_;
|
||||
ImagePtr thumbnail_;
|
||||
ThumbnailType thumbnailType_{};
|
||||
MessageElementFlags flags_;
|
||||
};
|
||||
|
||||
|
@ -269,15 +257,12 @@ public:
|
|||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
protected:
|
||||
QStringList words_;
|
||||
|
||||
private:
|
||||
MessageColor color_;
|
||||
FontStyle style_;
|
||||
|
||||
struct Word {
|
||||
QString text;
|
||||
int width = -1;
|
||||
};
|
||||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
// contains a text that will be truncated to one line
|
||||
|
@ -303,6 +288,40 @@ private:
|
|||
std::vector<Word> words_;
|
||||
};
|
||||
|
||||
class LinkElement : public TextElement
|
||||
{
|
||||
public:
|
||||
struct Parsed {
|
||||
QString lowercase;
|
||||
QString original;
|
||||
};
|
||||
|
||||
LinkElement(const Parsed &parsed, MessageElementFlags flags,
|
||||
const MessageColor &color = MessageColor::Text,
|
||||
FontStyle style = FontStyle::ChatMedium);
|
||||
~LinkElement() override = default;
|
||||
LinkElement(const LinkElement &) = delete;
|
||||
LinkElement(LinkElement &&) = delete;
|
||||
LinkElement &operator=(const LinkElement &) = delete;
|
||||
LinkElement &operator=(LinkElement &&) = delete;
|
||||
|
||||
void addToContainer(MessageLayoutContainer &container,
|
||||
MessageElementFlags flags) override;
|
||||
|
||||
Link getLink() const override;
|
||||
|
||||
[[nodiscard]] LinkInfo *linkInfo()
|
||||
{
|
||||
return &this->linkInfo_;
|
||||
}
|
||||
|
||||
private:
|
||||
LinkInfo linkInfo_;
|
||||
// these are implicitly shared
|
||||
QStringList lowercase_;
|
||||
QStringList original_;
|
||||
};
|
||||
|
||||
// contains emote data and will pick the emote based on :
|
||||
// a) are images for the emote type enabled
|
||||
// b) which size it wants
|
||||
|
|
|
@ -77,6 +77,7 @@ void SharedMessageBuilder::parse()
|
|||
if (this->action_)
|
||||
{
|
||||
this->textColor_ = this->usernameColor_;
|
||||
this->message().flags.set(MessageFlag::Action);
|
||||
}
|
||||
|
||||
this->parseUsername();
|
||||
|
|
|
@ -77,9 +77,9 @@ MessageLayoutElement *MessageLayoutElement::setTrailingSpace(bool value)
|
|||
return this;
|
||||
}
|
||||
|
||||
MessageLayoutElement *MessageLayoutElement::setLink(const Link &_link)
|
||||
MessageLayoutElement *MessageLayoutElement::setLink(const Link &link)
|
||||
{
|
||||
this->link_ = _link;
|
||||
this->link_ = link;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -89,9 +89,13 @@ MessageLayoutElement *MessageLayoutElement::setText(const QString &_text)
|
|||
return this;
|
||||
}
|
||||
|
||||
const Link &MessageLayoutElement::getLink() const
|
||||
Link MessageLayoutElement::getLink() const
|
||||
{
|
||||
return this->link_;
|
||||
if (this->link_)
|
||||
{
|
||||
return *this->link_;
|
||||
}
|
||||
return this->creator_.getLink();
|
||||
}
|
||||
|
||||
const QString &MessageLayoutElement::getText() const
|
||||
|
@ -406,14 +410,6 @@ TextLayoutElement::TextLayoutElement(MessageElement &_creator, QString &_text,
|
|||
this->setText(_text);
|
||||
}
|
||||
|
||||
void TextLayoutElement::listenToLinkChanges()
|
||||
{
|
||||
this->managedConnections_.managedConnect(
|
||||
static_cast<TextElement &>(this->getCreator()).linkChanged, [this]() {
|
||||
this->setLink(this->getCreator().getLink());
|
||||
});
|
||||
}
|
||||
|
||||
void TextLayoutElement::addCopyTextToString(QString &str, uint32_t from,
|
||||
uint32_t to) const
|
||||
{
|
||||
|
@ -504,7 +500,7 @@ int TextLayoutElement::getXFromIndex(size_t index)
|
|||
{
|
||||
return this->getRect().left();
|
||||
}
|
||||
else if (index < this->getText().size())
|
||||
else if (index < static_cast<size_t>(this->getText().size()))
|
||||
{
|
||||
int x = 0;
|
||||
for (int i = 0; i < index; i++)
|
||||
|
|
|
@ -44,7 +44,12 @@ public:
|
|||
void setLine(size_t line);
|
||||
|
||||
MessageLayoutElement *setTrailingSpace(bool value);
|
||||
MessageLayoutElement *setLink(const Link &link_);
|
||||
|
||||
/// @brief Overwrites the link for this layout element
|
||||
///
|
||||
/// @sa #getLink()
|
||||
MessageLayoutElement *setLink(const Link &link);
|
||||
|
||||
MessageLayoutElement *setText(const QString &text_);
|
||||
|
||||
virtual void addCopyTextToString(QString &str, uint32_t from = 0,
|
||||
|
@ -57,7 +62,12 @@ public:
|
|||
virtual int getMouseOverIndex(const QPoint &abs) const = 0;
|
||||
virtual int getXFromIndex(size_t index) = 0;
|
||||
|
||||
const Link &getLink() const;
|
||||
/// @brief Returns the link this layout element has
|
||||
///
|
||||
/// If there isn't any, an empty link is returned (type: None).
|
||||
/// The link is sourced from the creator, but can be overwritten with
|
||||
/// #setLink().
|
||||
Link getLink() const;
|
||||
const QString &getText() const;
|
||||
FlagsEnum<MessageElementFlag> getFlags() const;
|
||||
|
||||
|
@ -67,7 +77,7 @@ protected:
|
|||
private:
|
||||
QString text_;
|
||||
QRect rect_;
|
||||
Link link_;
|
||||
std::optional<Link> link_;
|
||||
MessageElement &creator_;
|
||||
/**
|
||||
* The line of the container this element is laid out at
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
#include "providers/LinkResolver.hpp"
|
||||
|
||||
#include "common/Env.hpp"
|
||||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "messages/Link.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void LinkResolver::getLinkInfo(
|
||||
const QString url, QObject *caller,
|
||||
std::function<void(QString, Link, ImagePtr)> successCallback)
|
||||
{
|
||||
if (!getSettings()->linkInfoTooltip)
|
||||
{
|
||||
successCallback("No link info loaded", Link(Link::Url, url), nullptr);
|
||||
return;
|
||||
}
|
||||
// Uncomment to test crashes
|
||||
// QTimer::singleShot(3000, [=]() {
|
||||
NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8(
|
||||
QUrl::toPercentEncoding(url, "", "/:"))))
|
||||
.caller(caller)
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback, url](NetworkResult result) mutable {
|
||||
auto root = result.parseJson();
|
||||
auto statusCode = root.value("status").toInt();
|
||||
QString response;
|
||||
QString linkString = url;
|
||||
ImagePtr thumbnail = nullptr;
|
||||
if (statusCode == 200)
|
||||
{
|
||||
response = root.value("tooltip").toString();
|
||||
|
||||
if (root.contains("thumbnail"))
|
||||
{
|
||||
thumbnail =
|
||||
Image::fromUrl({root.value("thumbnail").toString()});
|
||||
}
|
||||
if (getSettings()->unshortLinks)
|
||||
{
|
||||
linkString = root.value("link").toString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response = root.value("message").toString();
|
||||
}
|
||||
successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
|
||||
Link(Link::Url, linkString), thumbnail);
|
||||
})
|
||||
.onError([successCallback, url](auto /*result*/) {
|
||||
successCallback("No link info found", Link(Link::Url, url),
|
||||
nullptr);
|
||||
})
|
||||
.execute();
|
||||
// });
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,23 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Image;
|
||||
struct Link;
|
||||
using ImagePtr = std::shared_ptr<Image>;
|
||||
|
||||
class LinkResolver
|
||||
{
|
||||
public:
|
||||
static void getLinkInfo(
|
||||
const QString url, QObject *caller,
|
||||
std::function<void(QString, Link, ImagePtr)> callback);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -167,7 +167,7 @@ void Emojis::load()
|
|||
|
||||
void Emojis::loadEmojis()
|
||||
{
|
||||
// Current version: https://github.com/iamcal/emoji-data/blob/v14.0.0/emoji.json (Emoji version 14.0 (2022))
|
||||
// Current version: https://github.com/iamcal/emoji-data/blob/v15.1.1/emoji.json (Emoji version 15.1 (2023))
|
||||
QFile file(":/emoji.json");
|
||||
file.open(QFile::ReadOnly);
|
||||
QTextStream s1(&file);
|
||||
|
@ -269,14 +269,15 @@ void Emojis::loadEmojiSet()
|
|||
};
|
||||
// clang-format on
|
||||
|
||||
// As of emoji-data v15.1.1, google is the only source missing no images.
|
||||
if (!emoji->capabilities.contains(emojiSetToUse))
|
||||
{
|
||||
emojiSetToUse = "Twitter";
|
||||
emojiSetToUse = "Google";
|
||||
}
|
||||
|
||||
QString code = emoji->unifiedCode.toLower();
|
||||
QString urlPrefix =
|
||||
"https://pajbot.com/static/emoji-v2/img/twitter/64/";
|
||||
"https://pajbot.com/static/emoji-v2/img/google/64/";
|
||||
auto it = emojiSets.find(emojiSetToUse);
|
||||
if (it != emojiSets.end())
|
||||
{
|
||||
|
|
|
@ -42,8 +42,9 @@ std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id)
|
|||
return badges;
|
||||
}
|
||||
|
||||
std::optional<FfzBadges::Badge> FfzBadges::getBadge(const int badgeID)
|
||||
std::optional<FfzBadges::Badge> FfzBadges::getBadge(const int badgeID) const
|
||||
{
|
||||
this->tgBadges.guard();
|
||||
auto it = this->badges.find(badgeID);
|
||||
if (it != this->badges.end())
|
||||
{
|
||||
|
@ -62,6 +63,7 @@ void FfzBadges::load()
|
|||
std::unique_lock lock(this->mutex_);
|
||||
|
||||
auto jsonRoot = result.parseJson();
|
||||
this->tgBadges.guard();
|
||||
for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray())
|
||||
{
|
||||
auto jsonBadge = jsonBadge_.toObject();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "common/Aliases.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/ThreadGuard.hpp"
|
||||
|
||||
#include <QColor>
|
||||
|
||||
|
@ -30,10 +31,9 @@ public:
|
|||
};
|
||||
|
||||
std::vector<Badge> getUserBadges(const UserId &id);
|
||||
std::optional<Badge> getBadge(int badgeID) const;
|
||||
|
||||
private:
|
||||
std::optional<Badge> getBadge(int badgeID);
|
||||
|
||||
void load();
|
||||
|
||||
std::shared_mutex mutex_;
|
||||
|
@ -43,6 +43,7 @@ private:
|
|||
|
||||
// badges points a badge ID to the information about the badge
|
||||
std::unordered_map<int, Badge> badges;
|
||||
ThreadGuard tgBadges;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -169,6 +169,33 @@ EmoteMap ffz::detail::parseChannelEmotes(const QJsonObject &jsonRoot)
|
|||
return emotes;
|
||||
}
|
||||
|
||||
FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot)
|
||||
{
|
||||
FfzChannelBadgeMap channelBadges;
|
||||
|
||||
for (auto it = badgeRoot.begin(); it != badgeRoot.end(); ++it)
|
||||
{
|
||||
const auto badgeID = it.key().toInt();
|
||||
const auto &jsonUserIDs = it.value().toArray();
|
||||
for (const auto &jsonUserID : jsonUserIDs)
|
||||
{
|
||||
// NOTE: The Twitch User IDs come through as ints right now, the code below
|
||||
// tries to parse them as strings first since that's how we treat them anyway.
|
||||
if (jsonUserID.isString())
|
||||
{
|
||||
channelBadges[jsonUserID.toString()].emplace_back(badgeID);
|
||||
}
|
||||
else
|
||||
{
|
||||
channelBadges[QString::number(jsonUserID.toInt())].emplace_back(
|
||||
badgeID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return channelBadges;
|
||||
}
|
||||
|
||||
FfzEmotes::FfzEmotes()
|
||||
: global_(std::make_shared<EmoteMap>())
|
||||
{
|
||||
|
@ -220,6 +247,7 @@ void FfzEmotes::loadChannel(
|
|||
std::function<void(EmoteMap &&)> emoteCallback,
|
||||
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
|
||||
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
|
||||
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
|
||||
bool manualRefresh)
|
||||
{
|
||||
qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID;
|
||||
|
@ -229,8 +257,9 @@ void FfzEmotes::loadChannel(
|
|||
.timeout(20000)
|
||||
.onSuccess([emoteCallback = std::move(emoteCallback),
|
||||
modBadgeCallback = std::move(modBadgeCallback),
|
||||
vipBadgeCallback = std::move(vipBadgeCallback), channel,
|
||||
manualRefresh](const auto &result) {
|
||||
vipBadgeCallback = std::move(vipBadgeCallback),
|
||||
channelBadgesCallback = std::move(channelBadgesCallback),
|
||||
channel, manualRefresh](const auto &result) {
|
||||
const auto json = result.parseJson();
|
||||
|
||||
auto emoteMap = parseChannelEmotes(json);
|
||||
|
@ -238,12 +267,15 @@ void FfzEmotes::loadChannel(
|
|||
json["room"]["mod_urls"].toObject(), "Moderator");
|
||||
auto vipBadge = parseAuthorityBadge(
|
||||
json["room"]["vip_badge"].toObject(), "VIP");
|
||||
auto channelBadges =
|
||||
parseChannelBadges(json["room"]["user_badge_ids"].toObject());
|
||||
|
||||
bool hasEmotes = !emoteMap.empty();
|
||||
|
||||
emoteCallback(std::move(emoteMap));
|
||||
modBadgeCallback(std::move(modBadge));
|
||||
vipBadgeCallback(std::move(vipBadge));
|
||||
channelBadgesCallback(std::move(channelBadges));
|
||||
if (auto shared = channel.lock(); manualRefresh)
|
||||
{
|
||||
if (hasEmotes)
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <boost/unordered/unordered_flat_map.hpp>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <memory>
|
||||
|
@ -15,10 +17,19 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
|||
class EmoteMap;
|
||||
class Channel;
|
||||
|
||||
/// Maps a Twitch User ID to a list of badge IDs
|
||||
using FfzChannelBadgeMap =
|
||||
boost::unordered::unordered_flat_map<QString, std::vector<int>>;
|
||||
|
||||
namespace ffz::detail {
|
||||
|
||||
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot);
|
||||
|
||||
/**
|
||||
* Parse the `user_badge_ids` into a map of User IDs -> Badge IDs
|
||||
*/
|
||||
FfzChannelBadgeMap parseChannelBadges(const QJsonObject &badgeRoot);
|
||||
|
||||
} // namespace ffz::detail
|
||||
|
||||
class FfzEmotes final
|
||||
|
@ -35,6 +46,7 @@ public:
|
|||
std::function<void(EmoteMap &&)> emoteCallback,
|
||||
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
|
||||
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
|
||||
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
|
||||
bool manualRefresh);
|
||||
|
||||
private:
|
||||
|
|
106
src/providers/links/LinkInfo.cpp
Normal file
106
src/providers/links/LinkInfo.cpp
Normal file
|
@ -0,0 +1,106 @@
|
|||
#include "providers/links/LinkInfo.hpp"
|
||||
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
LinkInfo::LinkInfo(QString url)
|
||||
: QObject(nullptr)
|
||||
, originalUrl_(url)
|
||||
, url_(std::move(url))
|
||||
, tooltip_(this->url_)
|
||||
{
|
||||
}
|
||||
|
||||
LinkInfo::~LinkInfo() = default;
|
||||
|
||||
LinkInfo::State LinkInfo::state() const
|
||||
{
|
||||
return this->state_;
|
||||
}
|
||||
|
||||
QString LinkInfo::url() const
|
||||
{
|
||||
return this->url_;
|
||||
}
|
||||
|
||||
QString LinkInfo::originalUrl() const
|
||||
{
|
||||
return this->originalUrl_;
|
||||
}
|
||||
|
||||
bool LinkInfo::isPending() const
|
||||
{
|
||||
return this->state_ == State::Created;
|
||||
}
|
||||
|
||||
bool LinkInfo::isLoading() const
|
||||
{
|
||||
return this->state_ == State::Loading;
|
||||
}
|
||||
|
||||
bool LinkInfo::isLoaded() const
|
||||
{
|
||||
return this->state_ > State::Loading;
|
||||
}
|
||||
|
||||
bool LinkInfo::isResolved() const
|
||||
{
|
||||
return this->state_ == State::Resolved;
|
||||
}
|
||||
|
||||
bool LinkInfo::hasError() const
|
||||
{
|
||||
return this->state_ == State::Errored;
|
||||
}
|
||||
|
||||
bool LinkInfo::hasThumbnail() const
|
||||
{
|
||||
return this->thumbnail_ && !this->thumbnail_->url().string.isEmpty();
|
||||
}
|
||||
|
||||
QString LinkInfo::tooltip() const
|
||||
{
|
||||
return this->tooltip_;
|
||||
}
|
||||
|
||||
ImagePtr LinkInfo::thumbnail() const
|
||||
{
|
||||
return this->thumbnail_;
|
||||
}
|
||||
|
||||
void LinkInfo::setState(State state)
|
||||
{
|
||||
assertInGuiThread();
|
||||
assert(state >= this->state_);
|
||||
|
||||
if (this->state_ == state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = state;
|
||||
this->stateChanged(state);
|
||||
}
|
||||
|
||||
void LinkInfo::setResolvedUrl(QString resolvedUrl)
|
||||
{
|
||||
assertInGuiThread();
|
||||
this->url_ = std::move(resolvedUrl);
|
||||
}
|
||||
|
||||
void LinkInfo::setTooltip(QString tooltip)
|
||||
{
|
||||
assertInGuiThread();
|
||||
this->tooltip_ = std::move(tooltip);
|
||||
}
|
||||
|
||||
void LinkInfo::setThumbnail(ImagePtr thumbnail)
|
||||
{
|
||||
assertInGuiThread();
|
||||
this->thumbnail_ = std::move(thumbnail);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
135
src/providers/links/LinkInfo.hpp
Normal file
135
src/providers/links/LinkInfo.hpp
Normal file
|
@ -0,0 +1,135 @@
|
|||
#pragma once
|
||||
|
||||
#include "messages/Image.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief Rich info about a URL with tooltip and thumbnail
|
||||
///
|
||||
/// This is only a data class - it doesn't do the resolving.
|
||||
/// It can only be used from the GUI thread.
|
||||
class LinkInfo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// @brief the state of a link info
|
||||
///
|
||||
/// The state of a link can only increase. For example, it's not possible
|
||||
/// for the link to change from "Resolved" to "Loading".
|
||||
enum class State {
|
||||
/// @brief The object was created, no info is resolved
|
||||
///
|
||||
/// This is the initial state
|
||||
Created,
|
||||
/// Info is currently loading
|
||||
Loading,
|
||||
/// Info has been resolved and the properties have been updated
|
||||
Resolved,
|
||||
/// There has been an error resolving the link info (e.g. timeout)
|
||||
Errored,
|
||||
};
|
||||
|
||||
/// @brief Constructs a new link info for a URL
|
||||
///
|
||||
/// This doesn't load any link info.
|
||||
/// @see #ensureLoadingStarted()
|
||||
[[nodiscard]] explicit LinkInfo(QString url);
|
||||
|
||||
~LinkInfo() override;
|
||||
|
||||
LinkInfo(const LinkInfo &) = delete;
|
||||
LinkInfo(LinkInfo &&) = delete;
|
||||
LinkInfo &operator=(const LinkInfo &) = delete;
|
||||
LinkInfo &operator=(LinkInfo &&) = delete;
|
||||
|
||||
/// @brief The URL of this link
|
||||
///
|
||||
/// If the "unshortLinks" setting is enabled, this can change after the
|
||||
/// link is resolved.
|
||||
[[nodiscard]] QString url() const;
|
||||
|
||||
/// @brief The URL of this link as seen in the message
|
||||
///
|
||||
/// If the "unshortLinks" setting doesn't affect this URL.
|
||||
[[nodiscard]] QString originalUrl() const;
|
||||
|
||||
/// Returns the current state
|
||||
[[nodiscard]] State state() const;
|
||||
|
||||
/// Returns true if this link has not yet been resolved (it's "Created")
|
||||
[[nodiscard]] bool isPending() const;
|
||||
|
||||
/// Returns true if the info is loading
|
||||
[[nodiscard]] bool isLoading() const;
|
||||
|
||||
/// Returns true if the info is loaded (resolved or errored)
|
||||
[[nodiscard]] bool isLoaded() const;
|
||||
|
||||
/// Returns true if this link has been resolved
|
||||
[[nodiscard]] bool isResolved() const;
|
||||
|
||||
/// Returns true if the info failed to resolve
|
||||
[[nodiscard]] bool hasError() const;
|
||||
|
||||
/// Returns true if this link has a thumbnail
|
||||
[[nodiscard]] bool hasThumbnail() const;
|
||||
|
||||
/// @brief Returns the tooltip of this link
|
||||
///
|
||||
/// The tooltip contains the URL of the link and any info added by the
|
||||
/// resolver. Resolvers must include the URL.
|
||||
[[nodiscard]] QString tooltip() const;
|
||||
|
||||
/// @brief Returns the thumbnail of this link
|
||||
///
|
||||
/// The thumbnail is provided by the resolver and might not have been
|
||||
/// loaded yet.
|
||||
///
|
||||
/// @pre The caller must check #hasThumbnail() before calling this method
|
||||
[[nodiscard]] ImagePtr thumbnail() const;
|
||||
|
||||
/// @brief Updates the state and emits #stateChanged accordingly
|
||||
///
|
||||
/// @pre The caller must be in the GUI thread.
|
||||
/// @pre @a state must be greater or equal to the current state.
|
||||
/// @see #state(), #stateChanged
|
||||
void setState(State state);
|
||||
|
||||
/// @brief Updates the resolved url of this link
|
||||
///
|
||||
/// @pre The caller must be in the GUI thread.
|
||||
/// @see #url()
|
||||
void setResolvedUrl(QString resolvedUrl);
|
||||
|
||||
/// @brief Updates the tooltip of this link
|
||||
///
|
||||
/// @pre The caller must be in the GUI thread.
|
||||
/// @see #tooltip()
|
||||
void setTooltip(QString tooltip);
|
||||
|
||||
/// @brief Updates the thumbnail of this link
|
||||
///
|
||||
/// The thumbnail is allowed to be empty or nullptr.
|
||||
///
|
||||
/// @pre The caller must be in the GUI thread.
|
||||
/// @see #hasThumbnail(), #thumbnail()
|
||||
void setThumbnail(ImagePtr thumbnail);
|
||||
|
||||
signals:
|
||||
/// @brief Emitted when this link's state changes
|
||||
///
|
||||
/// @param state The new state
|
||||
void stateChanged(State state);
|
||||
|
||||
private:
|
||||
const QString originalUrl_;
|
||||
QString url_;
|
||||
|
||||
QString tooltip_;
|
||||
ImagePtr thumbnail_;
|
||||
|
||||
State state_ = State::Created;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
72
src/providers/links/LinkResolver.cpp
Normal file
72
src/providers/links/LinkResolver.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
#include "providers/links/LinkResolver.hpp"
|
||||
|
||||
#include "common/Env.hpp"
|
||||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
#include "providers/links/LinkInfo.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
#include <QStringBuilder>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void LinkResolver::resolve(LinkInfo *info)
|
||||
{
|
||||
using State = LinkInfo::State;
|
||||
|
||||
assert(info);
|
||||
|
||||
if (info->state() != State::Created)
|
||||
{
|
||||
// The link is already resolved or is currently loading
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getSettings()->linkInfoTooltip)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
info->setTooltip("Loading...");
|
||||
info->setState(State::Loading);
|
||||
|
||||
NetworkRequest(Env::get().linkResolverUrl.arg(QString::fromUtf8(
|
||||
QUrl::toPercentEncoding(info->originalUrl(), {}, "/:"))))
|
||||
.caller(info)
|
||||
.timeout(30000)
|
||||
.onSuccess([info](const NetworkResult &result) {
|
||||
const auto root = result.parseJson();
|
||||
QString response;
|
||||
QString url;
|
||||
ImagePtr thumbnail = nullptr;
|
||||
if (root["status"].toInt() == 200)
|
||||
{
|
||||
response = root["tooltip"].toString();
|
||||
|
||||
if (root.contains("thumbnail"))
|
||||
{
|
||||
info->setThumbnail(
|
||||
Image::fromUrl({root["thumbnail"].toString()}));
|
||||
}
|
||||
if (getSettings()->unshortLinks && root.contains("link"))
|
||||
{
|
||||
info->setResolvedUrl(root["link"].toString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response = root["message"].toString();
|
||||
}
|
||||
|
||||
info->setTooltip(QUrl::fromPercentEncoding(response.toUtf8()));
|
||||
info->setState(State::Resolved);
|
||||
})
|
||||
.onError([info](const auto &result) {
|
||||
info->setTooltip(u"No link info found (" % result.formatError() %
|
||||
u')');
|
||||
info->setState(State::Errored);
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
36
src/providers/links/LinkResolver.hpp
Normal file
36
src/providers/links/LinkResolver.hpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class LinkInfo;
|
||||
|
||||
class ILinkResolver
|
||||
{
|
||||
public:
|
||||
ILinkResolver() = default;
|
||||
virtual ~ILinkResolver() = default;
|
||||
ILinkResolver(const ILinkResolver &) = delete;
|
||||
ILinkResolver(ILinkResolver &&) = delete;
|
||||
ILinkResolver &operator=(const ILinkResolver &) = delete;
|
||||
ILinkResolver &operator=(ILinkResolver &&) = delete;
|
||||
|
||||
virtual void resolve(LinkInfo *info) = 0;
|
||||
};
|
||||
|
||||
class LinkResolver : public ILinkResolver
|
||||
{
|
||||
public:
|
||||
LinkResolver() = default;
|
||||
|
||||
/// @brief Loads and updates the link info
|
||||
///
|
||||
/// Calling this with an already resolved or currently loading info is a
|
||||
/// no-op. Loading can be blocked by disabling the "linkInfoTooltip"
|
||||
/// setting. URLs will be unshortened if the "unshortLinks" setting is
|
||||
/// enabled. The resolver is set through Env::linkResolverUrl.
|
||||
///
|
||||
/// @pre @a info must not be nullptr
|
||||
void resolve(LinkInfo *info) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -8,4 +8,16 @@ Message::Message(QJsonObject _json)
|
|||
{
|
||||
}
|
||||
|
||||
std::optional<Message> parseBaseMessage(const QString &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Message(jsonDoc.object());
|
||||
}
|
||||
|
||||
} // namespace chatterino::seventv::eventapi
|
||||
|
|
|
@ -28,16 +28,6 @@ std::optional<InnerClass> Message::toInner()
|
|||
return InnerClass{this->data};
|
||||
}
|
||||
|
||||
static std::optional<Message> parseBaseMessage(const QString &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return Message(jsonDoc.object());
|
||||
}
|
||||
std::optional<Message> parseBaseMessage(const QString &blob);
|
||||
|
||||
} // namespace chatterino::seventv::eventapi
|
||||
|
|
|
@ -20,21 +20,10 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchBadges::TwitchBadges()
|
||||
{
|
||||
this->loadTwitchBadges();
|
||||
}
|
||||
|
||||
void TwitchBadges::loadTwitchBadges()
|
||||
{
|
||||
assert(this->loaded_ == false);
|
||||
|
||||
if (!getHelix())
|
||||
{
|
||||
// This is intended for tests and benchmarks.
|
||||
return;
|
||||
}
|
||||
|
||||
getHelix()->getGlobalBadges(
|
||||
[this](auto globalBadges) {
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
|
|
@ -32,8 +32,6 @@ class TwitchBadges
|
|||
using BadgeIconCallback = std::function<void(QString, const QIconPtr)>;
|
||||
|
||||
public:
|
||||
TwitchBadges();
|
||||
|
||||
// Get badge from name and version
|
||||
std::optional<EmotePtr> badge(const QString &set,
|
||||
const QString &version) const;
|
||||
|
@ -45,8 +43,9 @@ public:
|
|||
void getBadgeIcons(const QList<DisplayBadge> &badges,
|
||||
BadgeIconCallback callback);
|
||||
|
||||
private:
|
||||
void loadTwitchBadges();
|
||||
|
||||
private:
|
||||
void parseTwitchBadges(QJsonObject root);
|
||||
void loaded();
|
||||
void loadEmoteImage(const QString &name, ImagePtr image,
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/bttv/BttvLiveUpdates.hpp"
|
||||
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/recentmessages/Api.hpp"
|
||||
#include "providers/seventv/eventapi/Dispatch.hpp"
|
||||
|
@ -164,12 +165,14 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
MessageBuilder builder;
|
||||
TwitchMessageBuilder::liveSystemMessage(this->getDisplayName(),
|
||||
&builder);
|
||||
builder.message().id = this->roomId();
|
||||
this->addMessage(builder.release());
|
||||
|
||||
// Message in /live channel
|
||||
MessageBuilder builder2;
|
||||
TwitchMessageBuilder::liveMessage(this->getDisplayName(),
|
||||
&builder2);
|
||||
builder2.message().id = this->roomId();
|
||||
getApp()->twitch->liveChannel->addMessage(builder2.release());
|
||||
|
||||
// Notify on all channels with a ping sound
|
||||
|
@ -197,14 +200,12 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
|
||||
// MSVC hates this code if the parens are not there
|
||||
int end = (std::max)(0, snapshotLength - 200);
|
||||
auto liveMessageSearchText =
|
||||
QString("%1 is live!").arg(this->getDisplayName());
|
||||
|
||||
for (int i = snapshotLength - 1; i >= end; --i)
|
||||
{
|
||||
const auto &s = snapshot[i];
|
||||
|
||||
if (s->messageText == liveMessageSearchText)
|
||||
if (s->id == this->roomId())
|
||||
{
|
||||
s->flags.set(MessageFlag::Disabled);
|
||||
break;
|
||||
|
@ -333,6 +334,14 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
|
|||
std::forward<decltype(vipBadge)>(vipBadge));
|
||||
}
|
||||
},
|
||||
[this, weak = weakOf<Channel>(this)](auto &&channelBadges) {
|
||||
if (auto shared = weak.lock())
|
||||
{
|
||||
this->tgFfzChannelBadges_.guard();
|
||||
this->ffzChannelBadges_ =
|
||||
std::forward<decltype(channelBadges)>(channelBadges);
|
||||
}
|
||||
},
|
||||
manualRefresh);
|
||||
}
|
||||
|
||||
|
@ -467,9 +476,19 @@ void TwitchChannel::updateStreamStatus(
|
|||
auto diff = since.secsTo(QDateTime::currentDateTime());
|
||||
status->uptime = QString::number(diff / 3600) + "h " +
|
||||
QString::number(diff % 3600 / 60) + "m";
|
||||
status->uptimeSeconds = diff;
|
||||
|
||||
status->rerun = false;
|
||||
status->streamType = stream.type;
|
||||
for (const auto &tag : stream.tags)
|
||||
{
|
||||
if (QString::compare(tag, "Rerun", Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
status->rerun = true;
|
||||
status->streamType = "rerun";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->setLive(true))
|
||||
{
|
||||
|
@ -796,6 +815,11 @@ bool TwitchChannel::isLive() const
|
|||
return this->streamStatus_.accessConst()->live;
|
||||
}
|
||||
|
||||
bool TwitchChannel::isRerun() const
|
||||
{
|
||||
return this->streamStatus_.accessConst()->rerun;
|
||||
}
|
||||
|
||||
SharedAccessGuard<const TwitchChannel::StreamStatus>
|
||||
TwitchChannel::accessStreamStatus() const
|
||||
{
|
||||
|
@ -1692,6 +1716,33 @@ std::optional<EmotePtr> TwitchChannel::twitchBadge(const QString &set,
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<FfzBadges::Badge> TwitchChannel::ffzChannelBadges(
|
||||
const QString &userID) const
|
||||
{
|
||||
this->tgFfzChannelBadges_.guard();
|
||||
|
||||
auto it = this->ffzChannelBadges_.find(userID);
|
||||
if (it == this->ffzChannelBadges_.end())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<FfzBadges::Badge> badges;
|
||||
|
||||
const auto *ffzBadges = getIApp()->getFfzBadges();
|
||||
|
||||
for (const auto &badgeID : it->second)
|
||||
{
|
||||
auto badge = ffzBadges->getBadge(badgeID);
|
||||
if (badge.has_value())
|
||||
{
|
||||
badges.emplace_back(*badge);
|
||||
}
|
||||
}
|
||||
|
||||
return badges;
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
|
||||
{
|
||||
return this->ffzCustomModBadge_.get();
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
#include "common/ChannelChatters.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/ThreadGuard.hpp"
|
||||
|
||||
#include <boost/circular_buffer/space_optimized.hpp>
|
||||
#include <boost/signals2.hpp>
|
||||
|
@ -82,6 +85,7 @@ public:
|
|||
QString game;
|
||||
QString gameId;
|
||||
QString uptime;
|
||||
int uptimeSeconds = 0;
|
||||
QString streamType;
|
||||
};
|
||||
|
||||
|
@ -137,6 +141,7 @@ public:
|
|||
const QString &popoutPlayerUrl();
|
||||
int chatterCount() const;
|
||||
bool isLive() const override;
|
||||
bool isRerun() const override;
|
||||
QString roomId() const;
|
||||
SharedAccessGuard<const RoomModes> accessRoomModes() const;
|
||||
SharedAccessGuard<const StreamStatus> accessStreamStatus() const;
|
||||
|
@ -198,6 +203,10 @@ public:
|
|||
std::optional<EmotePtr> ffzCustomVipBadge() const;
|
||||
std::optional<EmotePtr> twitchBadge(const QString &set,
|
||||
const QString &version) const;
|
||||
/**
|
||||
* Returns a list of channel-specific FrankerFaceZ badges for the given user
|
||||
*/
|
||||
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
|
||||
|
||||
// Cheers
|
||||
std::optional<CheerEmote> cheerEmote(const QString &string) const;
|
||||
|
@ -262,6 +271,12 @@ public:
|
|||
void updateStreamStatus(const std::optional<HelixStream> &helixStream);
|
||||
void updateStreamTitle(const QString &title);
|
||||
|
||||
/**
|
||||
* Returns the display name of the user
|
||||
*
|
||||
* If the display name contained chinese, japenese, or korean characters, the user's login name is returned instead
|
||||
**/
|
||||
const QString &getDisplayName() const override;
|
||||
void updateDisplayName(const QString &displayName);
|
||||
|
||||
private:
|
||||
|
@ -321,13 +336,6 @@ private:
|
|||
void setDisplayName(const QString &name);
|
||||
void setLocalizedName(const QString &name);
|
||||
|
||||
/**
|
||||
* Returns the display name of the user
|
||||
*
|
||||
* If the display name contained chinese, japenese, or korean characters, the user's login name is returned instead
|
||||
**/
|
||||
const QString &getDisplayName() const override;
|
||||
|
||||
/**
|
||||
* Returns the localized name of the user
|
||||
**/
|
||||
|
@ -392,6 +400,9 @@ protected:
|
|||
Atomic<std::optional<EmotePtr>> ffzCustomModBadge_;
|
||||
Atomic<std::optional<EmotePtr>> ffzCustomVipBadge_;
|
||||
|
||||
FfzChannelBadgeMap ffzChannelBadges_;
|
||||
ThreadGuard tgFfzChannelBadges_;
|
||||
|
||||
private:
|
||||
// Badges
|
||||
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
|
||||
|
|
|
@ -525,6 +525,11 @@ const IndirectChannel &TwitchIrcServer::getWatchingChannel() const
|
|||
return this->watchingChannel;
|
||||
}
|
||||
|
||||
QString TwitchIrcServer::getLastUserThatWhisperedMe() const
|
||||
{
|
||||
return this->lastUserThatWhisperedMe.get();
|
||||
}
|
||||
|
||||
void TwitchIrcServer::reloadBTTVGlobalEmotes()
|
||||
{
|
||||
getIApp()->getBttvEmotes()->loadEmotes();
|
||||
|
|
|
@ -29,6 +29,8 @@ public:
|
|||
|
||||
virtual const IndirectChannel &getWatchingChannel() const = 0;
|
||||
|
||||
virtual QString getLastUserThatWhisperedMe() const = 0;
|
||||
|
||||
// Update this interface with TwitchIrcServer methods as needed
|
||||
};
|
||||
|
||||
|
@ -81,6 +83,8 @@ public:
|
|||
|
||||
const IndirectChannel &getWatchingChannel() const override;
|
||||
|
||||
QString getLastUserThatWhisperedMe() const override;
|
||||
|
||||
protected:
|
||||
void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) override;
|
||||
|
|
|
@ -269,6 +269,128 @@ namespace {
|
|||
builder->message().badgeInfos = badgeInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes (only) the replacement of @a match in @a source.
|
||||
* The parts before and after the match in @a source are ignored.
|
||||
*
|
||||
* Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced
|
||||
* with the string captured by the corresponding capturing group.
|
||||
* This function should only be used if the regex contains capturing groups.
|
||||
*
|
||||
* Since Qt doesn't provide a way of replacing a single match with some replacement
|
||||
* while supporting both capturing groups and lookahead/-behind in the regex,
|
||||
* this is included here. It's essentially the implementation of
|
||||
* QString::replace(const QRegularExpression &, const QString &).
|
||||
* @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703
|
||||
*/
|
||||
QString makeRegexReplacement(QStringView source,
|
||||
const QRegularExpression ®ex,
|
||||
const QRegularExpressionMatch &match,
|
||||
const QString &replacement)
|
||||
{
|
||||
using SizeType = QString::size_type;
|
||||
struct QStringCapture {
|
||||
SizeType pos;
|
||||
SizeType len;
|
||||
int captureNumber;
|
||||
};
|
||||
|
||||
qsizetype numCaptures = regex.captureCount();
|
||||
|
||||
// 1. build the backreferences list, holding where the backreferences
|
||||
// are in the replacement string
|
||||
QVarLengthArray<QStringCapture> backReferences;
|
||||
|
||||
SizeType replacementLength = replacement.size();
|
||||
for (SizeType i = 0; i < replacementLength - 1; i++)
|
||||
{
|
||||
if (replacement[i] != u'\\')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int no = replacement[i + 1].digitValue();
|
||||
if (no <= 0 || no > numCaptures)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QStringCapture backReference{.pos = i, .len = 2};
|
||||
|
||||
if (i < replacementLength - 2)
|
||||
{
|
||||
int secondDigit = replacement[i + 2].digitValue();
|
||||
if (secondDigit != -1 &&
|
||||
((no * 10) + secondDigit) <= numCaptures)
|
||||
{
|
||||
no = (no * 10) + secondDigit;
|
||||
++backReference.len;
|
||||
}
|
||||
}
|
||||
|
||||
backReference.captureNumber = no;
|
||||
backReferences.append(backReference);
|
||||
}
|
||||
|
||||
// 2. iterate on the matches.
|
||||
// For every match, copy the replacement string in chunks
|
||||
// with the proper replacements for the backreferences
|
||||
|
||||
// length of the new string, with all the replacements
|
||||
SizeType newLength = 0;
|
||||
QVarLengthArray<QStringView> chunks;
|
||||
QStringView replacementView{replacement};
|
||||
|
||||
// Initially: empty, as we only care about the replacement
|
||||
SizeType len = 0;
|
||||
SizeType lastEnd = 0;
|
||||
for (const QStringCapture &backReference :
|
||||
std::as_const(backReferences))
|
||||
{
|
||||
// part of "replacement" before the backreference
|
||||
len = backReference.pos - lastEnd;
|
||||
if (len > 0)
|
||||
{
|
||||
chunks << replacementView.mid(lastEnd, len);
|
||||
newLength += len;
|
||||
}
|
||||
|
||||
// backreference itself
|
||||
len = match.capturedLength(backReference.captureNumber);
|
||||
if (len > 0)
|
||||
{
|
||||
chunks << source.mid(
|
||||
match.capturedStart(backReference.captureNumber), len);
|
||||
newLength += len;
|
||||
}
|
||||
|
||||
lastEnd = backReference.pos + backReference.len;
|
||||
}
|
||||
|
||||
// add the last part of the replacement string
|
||||
len = replacementView.size() - lastEnd;
|
||||
if (len > 0)
|
||||
{
|
||||
chunks << replacementView.mid(lastEnd, len);
|
||||
newLength += len;
|
||||
}
|
||||
|
||||
// 3. assemble the chunks together
|
||||
QString dst;
|
||||
dst.reserve(newLength);
|
||||
for (const QStringView &chunk : std::as_const(chunks))
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
|
||||
static_assert(sizeof(QChar) == sizeof(decltype(*chunk.utf16())));
|
||||
dst.append(reinterpret_cast<const QChar *>(chunk.utf16()),
|
||||
chunk.length());
|
||||
#else
|
||||
dst += chunk;
|
||||
#endif
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TwitchMessageBuilder::TwitchMessageBuilder(
|
||||
|
@ -419,7 +541,9 @@ MessagePtr TwitchMessageBuilder::build()
|
|||
this->tags, this->originalMessage_, this->messageOffset_);
|
||||
|
||||
// This runs through all ignored phrases and runs its replacements on this->originalMessage_
|
||||
this->runIgnoreReplaces(twitchEmotes);
|
||||
TwitchMessageBuilder::processIgnorePhrases(
|
||||
*getSettings()->ignoredMessages.readOnly(), this->originalMessage_,
|
||||
twitchEmotes);
|
||||
|
||||
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
|
@ -440,9 +564,10 @@ MessagePtr TwitchMessageBuilder::build()
|
|||
this->stylizeUsername(this->userName, this->message());
|
||||
|
||||
this->message().messageText = this->originalMessage_;
|
||||
this->message().searchText = stylizedUsername + " " +
|
||||
this->message().localizedName + " " +
|
||||
this->userName + ": " + this->originalMessage_;
|
||||
this->message().searchText =
|
||||
stylizedUsername + " " + this->message().localizedName + " " +
|
||||
this->userName + ": " + this->originalMessage_ + " " +
|
||||
this->message().searchText;
|
||||
|
||||
// highlights
|
||||
this->parseHighlights();
|
||||
|
@ -772,11 +897,16 @@ void TwitchMessageBuilder::parseThread()
|
|||
threadRoot->usernameColor, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::UserInfo, threadRoot->displayName});
|
||||
|
||||
MessageColor color = MessageColor::Text;
|
||||
if (threadRoot->flags.has(MessageFlag::Action))
|
||||
{
|
||||
color = threadRoot->usernameColor;
|
||||
}
|
||||
this->emplace<SingleLineTextElement>(
|
||||
threadRoot->messageText,
|
||||
MessageElementFlags({MessageElementFlag::RepliedMessage,
|
||||
MessageElementFlag::Text}),
|
||||
this->textColor_, FontStyle::ChatMediumSmall)
|
||||
color, FontStyle::ChatMediumSmall)
|
||||
->setLink({Link::ViewThread, this->thread_->rootId()});
|
||||
}
|
||||
else if (this->tags.find("reply-parent-msg-id") != this->tags.end())
|
||||
|
@ -960,12 +1090,12 @@ void TwitchMessageBuilder::appendUsername()
|
|||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::runIgnoreReplaces(
|
||||
void TwitchMessageBuilder::processIgnorePhrases(
|
||||
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
|
||||
{
|
||||
using SizeType = QString::size_type;
|
||||
|
||||
auto phrases = getSettings()->ignoredMessages.readOnly();
|
||||
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
|
||||
// all emotes outside the range come before `it`
|
||||
// all emotes in the range start at `it`
|
||||
|
@ -1034,20 +1164,20 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
|
||||
SizeType length, const QString &replacement) {
|
||||
auto removedEmotes = removeEmotesInRange(from, length);
|
||||
this->originalMessage_.replace(from, length, replacement);
|
||||
originalMessage.replace(from, length, replacement);
|
||||
auto wordStart = from;
|
||||
while (wordStart > 0)
|
||||
{
|
||||
if (this->originalMessage_[wordStart - 1] == ' ')
|
||||
if (originalMessage[wordStart - 1] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
--wordStart;
|
||||
}
|
||||
auto wordEnd = from + replacement.length();
|
||||
while (wordEnd < this->originalMessage_.length())
|
||||
while (wordEnd < originalMessage.length())
|
||||
{
|
||||
if (this->originalMessage_[wordEnd] == ' ')
|
||||
if (originalMessage[wordEnd] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
@ -1058,11 +1188,11 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
static_cast<int>(replacement.length() - length));
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
auto midExtendedRef = QStringView{this->originalMessage_}.mid(
|
||||
wordStart, wordEnd - wordStart);
|
||||
auto midExtendedRef =
|
||||
QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart);
|
||||
#else
|
||||
auto midExtendedRef =
|
||||
this->originalMessage_.midRef(wordStart, wordEnd - wordStart);
|
||||
originalMessage.midRef(wordStart, wordEnd - wordStart);
|
||||
#endif
|
||||
|
||||
for (auto &emote : removedEmotes)
|
||||
|
@ -1088,7 +1218,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
addReplEmotes(phrase, midExtendedRef, wordStart);
|
||||
};
|
||||
|
||||
for (const auto &phrase : *phrases)
|
||||
for (const auto &phrase : phrases)
|
||||
{
|
||||
if (phrase.isBlock())
|
||||
{
|
||||
|
@ -1110,16 +1240,22 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
QRegularExpressionMatch match;
|
||||
size_t iterations = 0;
|
||||
SizeType from = 0;
|
||||
while ((from = this->originalMessage_.indexOf(regex, from,
|
||||
&match)) != -1)
|
||||
while ((from = originalMessage.indexOf(regex, from, &match)) != -1)
|
||||
{
|
||||
auto replacement = phrase.getReplace();
|
||||
if (regex.captureCount() > 0)
|
||||
{
|
||||
replacement = makeRegexReplacement(originalMessage, regex,
|
||||
match, replacement);
|
||||
}
|
||||
|
||||
replaceMessageAt(phrase, from, match.capturedLength(),
|
||||
phrase.getReplace());
|
||||
replacement);
|
||||
from += phrase.getReplace().length();
|
||||
iterations++;
|
||||
if (iterations >= 128)
|
||||
{
|
||||
this->originalMessage_ =
|
||||
originalMessage =
|
||||
u"Too many replacements - check your ignores!"_s;
|
||||
return;
|
||||
}
|
||||
|
@ -1129,8 +1265,8 @@ void TwitchMessageBuilder::runIgnoreReplaces(
|
|||
}
|
||||
|
||||
SizeType from = 0;
|
||||
while ((from = this->originalMessage_.indexOf(
|
||||
pattern, from, phrase.caseSensitivity())) != -1)
|
||||
while ((from = originalMessage.indexOf(pattern, from,
|
||||
phrase.caseSensitivity())) != -1)
|
||||
{
|
||||
replaceMessageAt(phrase, from, pattern.length(),
|
||||
phrase.getReplace());
|
||||
|
@ -1311,6 +1447,18 @@ void TwitchMessageBuilder::appendFfzBadges()
|
|||
this->emplace<FfzBadgeElement>(
|
||||
badge.emote, MessageElementFlag::BadgeFfz, badge.color);
|
||||
}
|
||||
|
||||
if (this->twitchChannel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &badge :
|
||||
this->twitchChannel->ffzChannelBadges(this->userId_))
|
||||
{
|
||||
this->emplace<FfzBadgeElement>(
|
||||
badge.emote, MessageElementFlag::BadgeFfz, badge.color);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::appendSeventvBadges()
|
||||
|
|
|
@ -20,6 +20,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
|||
class Channel;
|
||||
class TwitchChannel;
|
||||
class MessageThread;
|
||||
class IgnorePhrase;
|
||||
struct HelixVip;
|
||||
using HelixModerator = HelixVip;
|
||||
struct ChannelPointReward;
|
||||
|
@ -108,6 +109,10 @@ public:
|
|||
const QVariantMap &tags, const QString &originalMessage,
|
||||
int messageOffset);
|
||||
|
||||
static void processIgnorePhrases(
|
||||
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
|
||||
private:
|
||||
void parseUsernameColor() override;
|
||||
void parseUsername() override;
|
||||
|
@ -118,8 +123,6 @@ private:
|
|||
void parseThread();
|
||||
void appendUsername();
|
||||
|
||||
void runIgnoreReplaces(std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
|
||||
Outcome tryAppendEmote(const EmoteName &name) override;
|
||||
|
||||
void addWords(const QStringList &words,
|
||||
|
|
|
@ -2703,8 +2703,12 @@ void Helix::updateShieldMode(
|
|||
Qt::CaseInsensitive))
|
||||
{
|
||||
failureCallback(Error::UserMissingScope, message);
|
||||
break;
|
||||
}
|
||||
|
||||
failureCallback(Error::Forwarded, message);
|
||||
}
|
||||
break;
|
||||
case 401: {
|
||||
failureCallback(Error::Forwarded, message);
|
||||
}
|
||||
|
|
|
@ -69,6 +69,9 @@ struct HelixStream {
|
|||
QString language;
|
||||
QString thumbnailUrl;
|
||||
|
||||
// This is the names, the IDs are now always empty
|
||||
std::vector<QString> tags;
|
||||
|
||||
HelixStream()
|
||||
: id("")
|
||||
, userId("")
|
||||
|
@ -99,6 +102,11 @@ struct HelixStream {
|
|||
, language(jsonObject.value("language").toString())
|
||||
, thumbnailUrl(jsonObject.value("thumbnail_url").toString())
|
||||
{
|
||||
const auto jsonTags = jsonObject.value("tags").toArray();
|
||||
for (const auto &tag : jsonTags)
|
||||
{
|
||||
this->tags.push_back(tag.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -16,4 +16,16 @@ PubSubMessage::PubSubMessage(QJsonObject _object)
|
|||
}
|
||||
}
|
||||
|
||||
std::optional<PubSubMessage> parsePubSubBaseMessage(const QString &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return PubSubMessage(jsonDoc.object());
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -45,17 +45,7 @@ std::optional<InnerClass> PubSubMessage::toInner()
|
|||
return InnerClass{this->nonce, data};
|
||||
}
|
||||
|
||||
static std::optional<PubSubMessage> parsePubSubBaseMessage(const QString &blob)
|
||||
{
|
||||
QJsonDocument jsonDoc(QJsonDocument::fromJson(blob.toUtf8()));
|
||||
|
||||
if (jsonDoc.isNull())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return PubSubMessage(jsonDoc.object());
|
||||
}
|
||||
std::optional<PubSubMessage> parsePubSubBaseMessage(const QString &blob);
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
|
|
|
@ -238,7 +238,7 @@ void ImageUploader::handleSuccessfulUpload(const NetworkResult &result,
|
|||
}
|
||||
else
|
||||
{
|
||||
QTimer::singleShot(UPLOAD_DELAY, [channel, &textEdit, this]() {
|
||||
QTimer::singleShot(UPLOAD_DELAY, [channel, textEdit, this]() {
|
||||
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
|
||||
textEdit);
|
||||
this->uploadQueue_.pop();
|
||||
|
@ -259,8 +259,11 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|||
}
|
||||
|
||||
channel->addMessage(makeSystemMessage(QString("Started upload...")));
|
||||
if (source->hasUrls())
|
||||
{
|
||||
auto tryUploadFromUrls = [&]() -> bool {
|
||||
if (!source->hasUrls())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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.
|
||||
|
@ -277,8 +280,7 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QString("Couldn't load image :(")));
|
||||
this->uploadMutex_.unlock();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto imageData = convertToPng(img);
|
||||
|
@ -293,8 +295,7 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|||
QString("Cannot upload file: %1. Couldn't convert "
|
||||
"image to png.")
|
||||
.arg(localPath)));
|
||||
this->uploadMutex_.unlock();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (mime.inherits("image/gif"))
|
||||
|
@ -307,21 +308,12 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QString("Failed to open file. :(")));
|
||||
this->uploadMutex_.unlock();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// file.readAll() => might be a bit big but it /should/ work
|
||||
RawImageData data = {file.readAll(), "gif", localPath};
|
||||
this->uploadQueue_.push(data);
|
||||
file.close();
|
||||
// file.readAll() => might be a bit big but it /should/ work
|
||||
}
|
||||
else
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Cannot upload file: %1. Not an image.")
|
||||
.arg(localPath)));
|
||||
this->uploadMutex_.unlock();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this->uploadQueue_.empty())
|
||||
|
@ -329,40 +321,52 @@ void ImageUploader::upload(const QMimeData *source, ChannelPtr channel,
|
|||
this->sendImageUploadRequest(this->uploadQueue_.front(), channel,
|
||||
outputTextEdit);
|
||||
this->uploadQueue_.pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else 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);
|
||||
}
|
||||
else if (source->hasFormat("image/jpeg"))
|
||||
{
|
||||
this->sendImageUploadRequest({source->data("image/jpeg"), "jpeg", ""},
|
||||
channel, outputTextEdit);
|
||||
}
|
||||
else if (source->hasFormat("image/gif"))
|
||||
{
|
||||
this->sendImageUploadRequest({source->data("image/gif"), "gif", ""},
|
||||
channel, outputTextEdit);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
else
|
||||
{ // not PNG, try loading it into QImage and save it to a PNG.
|
||||
auto tryUploadDirectly = [&]() -> bool {
|
||||
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;
|
||||
}
|
||||
if (source->hasFormat("image/jpeg"))
|
||||
{
|
||||
this->sendImageUploadRequest(
|
||||
{source->data("image/jpeg"), "jpeg", ""}, channel,
|
||||
outputTextEdit);
|
||||
return true;
|
||||
}
|
||||
if (source->hasFormat("image/gif"))
|
||||
{
|
||||
this->sendImageUploadRequest({source->data("image/gif"), "gif", ""},
|
||||
channel, outputTextEdit);
|
||||
return true;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Cannot upload file, failed to convert to png.")));
|
||||
this->uploadMutex_.unlock();
|
||||
}
|
||||
// No direct upload happenned
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("Cannot upload file, failed to convert to png.")));
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!tryUploadFromUrls() && !tryUploadDirectly())
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QString("Cannot upload file from clipboard.")));
|
||||
this->uploadMutex_.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include "Application.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/Modes.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
@ -40,7 +41,7 @@ void registerNmManifest(const Paths &paths, const QString &manifestFilename,
|
|||
|
||||
void registerNmHost(const Paths &paths)
|
||||
{
|
||||
if (paths.isPortable())
|
||||
if (Modes::instance().isPortable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ void Paths::initRootDirectory()
|
|||
|
||||
this->rootAppDataDirectory = [&]() -> QString {
|
||||
// portable
|
||||
if (this->isPortable())
|
||||
if (Modes::instance().isPortable)
|
||||
{
|
||||
return QCoreApplication::applicationDirPath();
|
||||
}
|
||||
|
|
|
@ -218,6 +218,10 @@ public:
|
|||
"/behaviour/autocompletion/emoteCompletionWithColon", true};
|
||||
BoolSetting showUsernameCompletionMenu = {
|
||||
"/behaviour/autocompletion/showUsernameCompletionMenu", true};
|
||||
BoolSetting alwaysIncludeBroadcasterInUserCompletions = {
|
||||
"/behaviour/autocompletion/alwaysIncludeBroadcasterInUserCompletions",
|
||||
true,
|
||||
};
|
||||
BoolSetting useSmartEmoteCompletion = {
|
||||
"/experiments/useSmartEmoteCompletion",
|
||||
false,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "common/QLogging.hpp"
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDir>
|
||||
|
@ -13,6 +14,9 @@
|
|||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QSet>
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
# include <QStyleHints>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
|
||||
|
@ -21,67 +25,107 @@ namespace {
|
|||
using namespace chatterino;
|
||||
using namespace literals;
|
||||
|
||||
void parseInto(const QJsonObject &obj, QLatin1String key, QColor &color)
|
||||
void parseInto(const QJsonObject &obj, const QJsonObject &fallbackObj,
|
||||
QLatin1String key, QColor &color)
|
||||
{
|
||||
const auto &jsonValue = obj[key];
|
||||
if (!jsonValue.isString()) [[unlikely]]
|
||||
auto parseColorFrom = [](const auto &obj,
|
||||
QLatin1String key) -> std::optional<QColor> {
|
||||
auto jsonValue = obj[key];
|
||||
if (!jsonValue.isString()) [[unlikely]]
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
QColor parsed = {jsonValue.toString()};
|
||||
if (!parsed.isValid()) [[unlikely]]
|
||||
{
|
||||
qCWarning(chatterinoTheme).nospace()
|
||||
<< "While parsing " << key << ": '" << jsonValue.toString()
|
||||
<< "' isn't a valid color.";
|
||||
return std::nullopt;
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
|
||||
auto firstColor = parseColorFrom(obj, key);
|
||||
if (firstColor.has_value())
|
||||
{
|
||||
qCWarning(chatterinoTheme) << key
|
||||
<< "was expected but not found in the "
|
||||
"current theme - using previous value.";
|
||||
color = firstColor.value();
|
||||
return;
|
||||
}
|
||||
QColor parsed = {jsonValue.toString()};
|
||||
if (!parsed.isValid()) [[unlikely]]
|
||||
|
||||
if (!fallbackObj.isEmpty())
|
||||
{
|
||||
qCWarning(chatterinoTheme).nospace()
|
||||
<< "While parsing " << key << ": '" << jsonValue.toString()
|
||||
<< "' isn't a valid color.";
|
||||
return;
|
||||
auto fallbackColor = parseColorFrom(fallbackObj, key);
|
||||
if (fallbackColor.has_value())
|
||||
{
|
||||
color = fallbackColor.value();
|
||||
return;
|
||||
}
|
||||
}
|
||||
color = parsed;
|
||||
|
||||
qCWarning(chatterinoTheme) << key
|
||||
<< "was expected but not found in the "
|
||||
"current theme, and no fallback value found.";
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(cppcoreguidelines-macro-usage)
|
||||
#define _c2StringLit(s, ty) s##ty
|
||||
#define parseColor(to, from, key) \
|
||||
parseInto(from, _c2StringLit(#key, _L1), (to).from.key)
|
||||
parseInto(from, from##Fallback, _c2StringLit(#key, _L1), (to).from.key)
|
||||
// NOLINTEND(cppcoreguidelines-macro-usage)
|
||||
|
||||
void parseWindow(const QJsonObject &window, chatterino::Theme &theme)
|
||||
void parseWindow(const QJsonObject &window, const QJsonObject &windowFallback,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
parseColor(theme, window, background);
|
||||
parseColor(theme, window, text);
|
||||
}
|
||||
|
||||
void parseTabs(const QJsonObject &tabs, chatterino::Theme &theme)
|
||||
void parseTabs(const QJsonObject &tabs, const QJsonObject &tabsFallback,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
const auto parseTabColors = [](const auto &json, auto &tab) {
|
||||
parseInto(json, "text"_L1, tab.text);
|
||||
const auto parseTabColors = [](const auto &json, const auto &jsonFallback,
|
||||
auto &tab) {
|
||||
parseInto(json, jsonFallback, "text"_L1, tab.text);
|
||||
{
|
||||
const auto backgrounds = json["backgrounds"_L1].toObject();
|
||||
const auto backgroundsFallback =
|
||||
jsonFallback["backgrounds"_L1].toObject();
|
||||
parseColor(tab, backgrounds, regular);
|
||||
parseColor(tab, backgrounds, hover);
|
||||
parseColor(tab, backgrounds, unfocused);
|
||||
}
|
||||
{
|
||||
const auto line = json["line"_L1].toObject();
|
||||
const auto lineFallback = jsonFallback["line"_L1].toObject();
|
||||
parseColor(tab, line, regular);
|
||||
parseColor(tab, line, hover);
|
||||
parseColor(tab, line, unfocused);
|
||||
}
|
||||
};
|
||||
parseColor(theme, tabs, dividerLine);
|
||||
parseTabColors(tabs["regular"_L1].toObject(), theme.tabs.regular);
|
||||
parseTabColors(tabs["newMessage"_L1].toObject(), theme.tabs.newMessage);
|
||||
parseTabColors(tabs["highlighted"_L1].toObject(), theme.tabs.highlighted);
|
||||
parseTabColors(tabs["selected"_L1].toObject(), theme.tabs.selected);
|
||||
parseColor(theme, tabs, liveIndicator);
|
||||
parseColor(theme, tabs, rerunIndicator);
|
||||
parseTabColors(tabs["regular"_L1].toObject(),
|
||||
tabsFallback["regular"_L1].toObject(), theme.tabs.regular);
|
||||
parseTabColors(tabs["newMessage"_L1].toObject(),
|
||||
tabsFallback["newMessage"_L1].toObject(),
|
||||
theme.tabs.newMessage);
|
||||
parseTabColors(tabs["highlighted"_L1].toObject(),
|
||||
tabsFallback["highlighted"_L1].toObject(),
|
||||
theme.tabs.highlighted);
|
||||
parseTabColors(tabs["selected"_L1].toObject(),
|
||||
tabsFallback["selected"_L1].toObject(), theme.tabs.selected);
|
||||
}
|
||||
|
||||
void parseMessages(const QJsonObject &messages, chatterino::Theme &theme)
|
||||
void parseMessages(const QJsonObject &messages,
|
||||
const QJsonObject &messagesFallback,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
{
|
||||
const auto textColors = messages["textColors"_L1].toObject();
|
||||
const auto textColorsFallback =
|
||||
messagesFallback["textColors"_L1].toObject();
|
||||
parseColor(theme.messages, textColors, regular);
|
||||
parseColor(theme.messages, textColors, caret);
|
||||
parseColor(theme.messages, textColors, link);
|
||||
|
@ -90,6 +134,8 @@ void parseMessages(const QJsonObject &messages, chatterino::Theme &theme)
|
|||
}
|
||||
{
|
||||
const auto backgrounds = messages["backgrounds"_L1].toObject();
|
||||
const auto backgroundsFallback =
|
||||
messagesFallback["backgrounds"_L1].toObject();
|
||||
parseColor(theme.messages, backgrounds, regular);
|
||||
parseColor(theme.messages, backgrounds, alternate);
|
||||
}
|
||||
|
@ -99,14 +145,17 @@ void parseMessages(const QJsonObject &messages, chatterino::Theme &theme)
|
|||
parseColor(theme, messages, highlightAnimationEnd);
|
||||
}
|
||||
|
||||
void parseScrollbars(const QJsonObject &scrollbars, chatterino::Theme &theme)
|
||||
void parseScrollbars(const QJsonObject &scrollbars,
|
||||
const QJsonObject &scrollbarsFallback,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
parseColor(theme, scrollbars, background);
|
||||
parseColor(theme, scrollbars, thumb);
|
||||
parseColor(theme, scrollbars, thumbSelected);
|
||||
}
|
||||
|
||||
void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
|
||||
void parseSplits(const QJsonObject &splits, const QJsonObject &splitsFallback,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
parseColor(theme, splits, messageSeperator);
|
||||
parseColor(theme, splits, background);
|
||||
|
@ -119,6 +168,7 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
|
|||
|
||||
{
|
||||
const auto header = splits["header"_L1].toObject();
|
||||
const auto headerFallback = splitsFallback["header"_L1].toObject();
|
||||
parseColor(theme.splits, header, border);
|
||||
parseColor(theme.splits, header, focusedBorder);
|
||||
parseColor(theme.splits, header, background);
|
||||
|
@ -128,22 +178,30 @@ void parseSplits(const QJsonObject &splits, chatterino::Theme &theme)
|
|||
}
|
||||
{
|
||||
const auto input = splits["input"_L1].toObject();
|
||||
const auto inputFallback = splitsFallback["input"_L1].toObject();
|
||||
parseColor(theme.splits, input, background);
|
||||
parseColor(theme.splits, input, text);
|
||||
}
|
||||
}
|
||||
|
||||
void parseColors(const QJsonObject &root, chatterino::Theme &theme)
|
||||
void parseColors(const QJsonObject &root, const QJsonObject &fallbackTheme,
|
||||
chatterino::Theme &theme)
|
||||
{
|
||||
const auto colors = root["colors"_L1].toObject();
|
||||
const auto fallbackColors = fallbackTheme["colors"_L1].toObject();
|
||||
|
||||
parseInto(colors, "accent"_L1, theme.accent);
|
||||
parseInto(colors, fallbackColors, "accent"_L1, theme.accent);
|
||||
|
||||
parseWindow(colors["window"_L1].toObject(), theme);
|
||||
parseTabs(colors["tabs"_L1].toObject(), theme);
|
||||
parseMessages(colors["messages"_L1].toObject(), theme);
|
||||
parseScrollbars(colors["scrollbars"_L1].toObject(), theme);
|
||||
parseSplits(colors["splits"_L1].toObject(), theme);
|
||||
parseWindow(colors["window"_L1].toObject(),
|
||||
fallbackColors["window"_L1].toObject(), theme);
|
||||
parseTabs(colors["tabs"_L1].toObject(),
|
||||
fallbackColors["tabs"_L1].toObject(), theme);
|
||||
parseMessages(colors["messages"_L1].toObject(),
|
||||
fallbackColors["messages"_L1].toObject(), theme);
|
||||
parseScrollbars(colors["scrollbars"_L1].toObject(),
|
||||
fallbackColors["scrollbars"_L1].toObject(), theme);
|
||||
parseSplits(colors["splits"_L1].toObject(),
|
||||
fallbackColors["splits"_L1].toObject(), theme);
|
||||
}
|
||||
#undef parseColor
|
||||
#undef _c2StringLit
|
||||
|
@ -219,6 +277,11 @@ bool Theme::isLightTheme() const
|
|||
return this->isLight_;
|
||||
}
|
||||
|
||||
bool Theme::isSystemTheme() const
|
||||
{
|
||||
return this->themeName == u"System"_s;
|
||||
}
|
||||
|
||||
void Theme::initialize(Settings &settings, const Paths &paths)
|
||||
{
|
||||
this->themeName.connect(
|
||||
|
@ -227,15 +290,51 @@ void Theme::initialize(Settings &settings, const Paths &paths)
|
|||
this->update();
|
||||
},
|
||||
false);
|
||||
auto updateIfSystem = [this](const auto &) {
|
||||
if (this->isSystemTheme())
|
||||
{
|
||||
this->update();
|
||||
}
|
||||
};
|
||||
this->darkSystemThemeName.connect(updateIfSystem, false);
|
||||
this->lightSystemThemeName.connect(updateIfSystem, false);
|
||||
|
||||
this->loadAvailableThemes(paths);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
QObject::connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged,
|
||||
&this->lifetime_, [this] {
|
||||
if (this->isSystemTheme())
|
||||
{
|
||||
this->update();
|
||||
getIApp()->getWindows()->forceLayoutChannelViews();
|
||||
}
|
||||
});
|
||||
#endif
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
void Theme::update()
|
||||
{
|
||||
auto oTheme = this->findThemeByKey(this->themeName);
|
||||
auto currentTheme = [&]() -> QString {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
if (this->isSystemTheme())
|
||||
{
|
||||
switch (qApp->styleHints()->colorScheme())
|
||||
{
|
||||
case Qt::ColorScheme::Light:
|
||||
return this->lightSystemThemeName;
|
||||
case Qt::ColorScheme::Unknown:
|
||||
case Qt::ColorScheme::Dark:
|
||||
return this->darkSystemThemeName;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return this->themeName;
|
||||
};
|
||||
|
||||
auto oTheme = this->findThemeByKey(currentTheme());
|
||||
|
||||
constexpr const double nsToMs = 1.0 / 1000000.0;
|
||||
QElapsedTimer timer;
|
||||
|
@ -243,6 +342,7 @@ void Theme::update()
|
|||
|
||||
std::optional<QJsonObject> themeJSON;
|
||||
QString themePath;
|
||||
bool isCustomTheme = false;
|
||||
if (!oTheme)
|
||||
{
|
||||
qCWarning(chatterinoTheme)
|
||||
|
@ -269,6 +369,10 @@ void Theme::update()
|
|||
themeJSON = loadTheme(fallbackTheme);
|
||||
themePath = fallbackTheme.path;
|
||||
}
|
||||
else
|
||||
{
|
||||
isCustomTheme = theme.custom;
|
||||
}
|
||||
}
|
||||
auto loadTs = double(timer.nsecsElapsed()) * nsToMs;
|
||||
|
||||
|
@ -284,7 +388,7 @@ void Theme::update()
|
|||
return;
|
||||
}
|
||||
|
||||
this->parseFrom(*themeJSON);
|
||||
this->parseFrom(*themeJSON, isCustomTheme);
|
||||
this->currentThemePath_ = themePath;
|
||||
|
||||
auto parseTs = double(timer.nsecsElapsed()) * nsToMs;
|
||||
|
@ -375,13 +479,30 @@ std::optional<ThemeDescriptor> Theme::findThemeByKey(const QString &key)
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Theme::parseFrom(const QJsonObject &root)
|
||||
void Theme::parseFrom(const QJsonObject &root, bool isCustomTheme)
|
||||
{
|
||||
parseColors(root, *this);
|
||||
|
||||
this->isLight_ =
|
||||
root["metadata"_L1]["iconTheme"_L1].toString() == u"dark"_s;
|
||||
|
||||
std::optional<QJsonObject> fallbackTheme;
|
||||
if (isCustomTheme)
|
||||
{
|
||||
// Only attempt to load a fallback theme if the theme we're loading is a custom theme
|
||||
auto fallbackThemeName =
|
||||
root["metadata"_L1]["fallbackTheme"_L1].toString(
|
||||
this->isLightTheme() ? "Light" : "Dark");
|
||||
for (const auto &theme : Theme::builtInThemes)
|
||||
{
|
||||
if (fallbackThemeName.compare(theme.key, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
fallbackTheme = loadTheme(theme);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseColors(root, fallbackTheme.value_or(QJsonObject()), *this);
|
||||
|
||||
this->splits.input.styleSheet = uR"(
|
||||
background: %1;
|
||||
border: %2;
|
||||
|
|
|
@ -46,6 +46,7 @@ public:
|
|||
void initialize(Settings &settings, const Paths &paths) final;
|
||||
|
||||
bool isLightTheme() const;
|
||||
bool isSystemTheme() const;
|
||||
|
||||
struct TabColors {
|
||||
QColor text;
|
||||
|
@ -76,6 +77,9 @@ public:
|
|||
TabColors highlighted;
|
||||
TabColors selected;
|
||||
QColor dividerLine;
|
||||
|
||||
QColor liveIndicator;
|
||||
QColor rerunIndicator;
|
||||
} tabs;
|
||||
|
||||
/// MESSAGES
|
||||
|
@ -153,6 +157,9 @@ public:
|
|||
pajlada::Signals::NoArgSignal updated;
|
||||
|
||||
QStringSetting themeName{"/appearance/theme/name", "Dark"};
|
||||
QStringSetting lightSystemThemeName{"/appearance/theme/lightSystem",
|
||||
"Light"};
|
||||
QStringSetting darkSystemThemeName{"/appearance/theme/darkSystem", "Dark"};
|
||||
|
||||
private:
|
||||
bool isLight_ = false;
|
||||
|
@ -164,6 +171,8 @@ private:
|
|||
// This will only be populated when auto-reloading themes
|
||||
QJsonObject currentThemeJson_;
|
||||
|
||||
QObject lifetime_;
|
||||
|
||||
/**
|
||||
* Figure out which themes are available in the Themes directory
|
||||
*
|
||||
|
@ -173,7 +182,7 @@ private:
|
|||
|
||||
std::optional<ThemeDescriptor> findThemeByKey(const QString &key);
|
||||
|
||||
void parseFrom(const QJsonObject &root);
|
||||
void parseFrom(const QJsonObject &root, bool isCustomTheme);
|
||||
|
||||
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ void Updates::installUpdates()
|
|||
box->exec();
|
||||
QDesktopServices::openUrl(this->updateGuideLink_);
|
||||
#elif defined Q_OS_WIN
|
||||
if (this->paths.isPortable())
|
||||
if (Modes::instance().isPortable)
|
||||
{
|
||||
QMessageBox *box =
|
||||
new QMessageBox(QMessageBox::Information, "Chatterino Update",
|
||||
|
|
|
@ -185,8 +185,7 @@ void WindowManager::updateWordTypeMask()
|
|||
flags.set(MEF::Collapsed);
|
||||
flags.set(settings->boldUsernames ? MEF::BoldUsername
|
||||
: MEF::NonBoldUsername);
|
||||
flags.set(settings->lowercaseDomains ? MEF::LowercaseLink
|
||||
: MEF::OriginalLink);
|
||||
flags.set(MEF::LowercaseLinks, settings->lowercaseDomains);
|
||||
flags.set(MEF::ChannelPointReward);
|
||||
|
||||
// update flags
|
||||
|
|
|
@ -1,10 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/container_hash/hash_fwd.hpp>
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace boost {
|
||||
|
||||
template <>
|
||||
struct hash<QString> {
|
||||
std::size_t operator()(QString const &s) const
|
||||
{
|
||||
return qHash(s);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace boost
|
||||
|
||||
namespace std {
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
static auto defaultItemFlags(bool selectable)
|
||||
inline auto defaultItemFlags(bool selectable)
|
||||
{
|
||||
return Qt::ItemIsEnabled |
|
||||
(selectable ? Qt::ItemIsSelectable | Qt::ItemIsDragEnabled |
|
||||
|
@ -13,7 +13,7 @@ static auto defaultItemFlags(bool selectable)
|
|||
: Qt::ItemFlag());
|
||||
}
|
||||
|
||||
static void setBoolItem(QStandardItem *item, bool value,
|
||||
inline void setBoolItem(QStandardItem *item, bool value,
|
||||
bool userCheckable = true, bool selectable = true)
|
||||
{
|
||||
item->setFlags(
|
||||
|
@ -22,7 +22,7 @@ static void setBoolItem(QStandardItem *item, bool value,
|
|||
item->setCheckState(value ? Qt::Checked : Qt::Unchecked);
|
||||
}
|
||||
|
||||
static void setStringItem(QStandardItem *item, const QString &value,
|
||||
inline void setStringItem(QStandardItem *item, const QString &value,
|
||||
bool editable = true, bool selectable = true)
|
||||
{
|
||||
item->setData(value, Qt::EditRole);
|
||||
|
@ -30,7 +30,7 @@ static void setStringItem(QStandardItem *item, const QString &value,
|
|||
(editable ? (Qt::ItemIsEditable) : 0)));
|
||||
}
|
||||
|
||||
static void setFilePathItem(QStandardItem *item, const QUrl &value,
|
||||
inline void setFilePathItem(QStandardItem *item, const QUrl &value,
|
||||
bool selectable = true)
|
||||
{
|
||||
item->setData(value, Qt::UserRole);
|
||||
|
@ -40,7 +40,7 @@ static void setFilePathItem(QStandardItem *item, const QUrl &value,
|
|||
(selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags)));
|
||||
}
|
||||
|
||||
static void setColorItem(QStandardItem *item, const QColor &value,
|
||||
inline void setColorItem(QStandardItem *item, const QColor &value,
|
||||
bool selectable = true)
|
||||
{
|
||||
item->setData(value, Qt::DecorationRole);
|
||||
|
@ -49,7 +49,7 @@ static void setColorItem(QStandardItem *item, const QColor &value,
|
|||
(selectable ? Qt::ItemIsSelectable : Qt::NoItemFlags)));
|
||||
}
|
||||
|
||||
static QStandardItem *emptyItem()
|
||||
inline QStandardItem *emptyItem()
|
||||
{
|
||||
auto *item = new QStandardItem();
|
||||
item->setFlags(Qt::ItemFlags());
|
||||
|
|
|
@ -10,11 +10,11 @@ namespace chatterino {
|
|||
// Debug-class which asserts if guard of the same object has been called from different threads
|
||||
struct ThreadGuard {
|
||||
#ifndef NDEBUG
|
||||
std::mutex mutex;
|
||||
std::optional<std::thread::id> threadID;
|
||||
mutable std::mutex mutex;
|
||||
mutable std::optional<std::thread::id> threadID;
|
||||
#endif
|
||||
|
||||
inline void guard()
|
||||
inline void guard() const
|
||||
{
|
||||
#ifndef NDEBUG
|
||||
std::unique_lock lock(this->mutex);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "widgets/BaseWindow.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "singletons/WindowManager.hpp"
|
||||
|
@ -200,44 +201,76 @@ void BaseWindow::init()
|
|||
}
|
||||
|
||||
// DPI
|
||||
// auto dpi = getWindowDpi(this->winId());
|
||||
// auto dpi = getWindowDpi(this->safeHWND());
|
||||
|
||||
// if (dpi) {
|
||||
// this->scale = dpi.value() / 96.f;
|
||||
// }
|
||||
#endif
|
||||
|
||||
#ifdef USEWINSDK
|
||||
// fourtf: don't ask me why we need to delay this
|
||||
if (!this->flags_.has(TopMost))
|
||||
{
|
||||
QTimer::singleShot(1, this, [this] {
|
||||
getSettings()->windowTopMost.connect(
|
||||
[this](bool topMost, auto) {
|
||||
::SetWindowPos(HWND(this->winId()),
|
||||
topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0,
|
||||
0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
},
|
||||
this->connections_);
|
||||
});
|
||||
}
|
||||
#else
|
||||
// TopMost flag overrides setting
|
||||
if (!this->flags_.has(TopMost))
|
||||
{
|
||||
getSettings()->windowTopMost.connect(
|
||||
[this](bool topMost, auto) {
|
||||
auto isVisible = this->isVisible();
|
||||
this->setWindowFlag(Qt::WindowStaysOnTopHint, topMost);
|
||||
if (isVisible)
|
||||
{
|
||||
this->show();
|
||||
}
|
||||
[this](bool topMost) {
|
||||
this->setTopMost(topMost);
|
||||
},
|
||||
this->connections_);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseWindow::setTopMost(bool topMost)
|
||||
{
|
||||
if (this->flags_.has(TopMost))
|
||||
{
|
||||
qCWarning(chatterinoWidget)
|
||||
<< "Called setTopMost on a window with the `TopMost` flag set.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->isTopMost_ == topMost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->isTopMost_ = topMost;
|
||||
|
||||
#ifdef USEWINSDK
|
||||
if (!this->waitingForTopMost_)
|
||||
{
|
||||
this->tryApplyTopMost();
|
||||
}
|
||||
#else
|
||||
auto isVisible = this->isVisible();
|
||||
this->setWindowFlag(Qt::WindowStaysOnTopHint, topMost);
|
||||
if (isVisible)
|
||||
{
|
||||
this->show();
|
||||
}
|
||||
#endif
|
||||
|
||||
this->topMostChanged(this->isTopMost_);
|
||||
}
|
||||
|
||||
#ifdef USEWINSDK
|
||||
void BaseWindow::tryApplyTopMost()
|
||||
{
|
||||
auto hwnd = this->safeHWND();
|
||||
if (!hwnd)
|
||||
{
|
||||
this->waitingForTopMost_ = true;
|
||||
QTimer::singleShot(50, this, &BaseWindow::tryApplyTopMost);
|
||||
return;
|
||||
}
|
||||
this->waitingForTopMost_ = false;
|
||||
|
||||
::SetWindowPos(*hwnd, this->isTopMost_ ? HWND_TOPMOST : HWND_NOTOPMOST, 0,
|
||||
0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool BaseWindow::isTopMost() const
|
||||
{
|
||||
return this->isTopMost_ || this->flags_.has(TopMost);
|
||||
}
|
||||
|
||||
void BaseWindow::setActionOnFocusLoss(ActionOnFocusLoss value)
|
||||
|
@ -475,12 +508,15 @@ void BaseWindow::changeEvent(QEvent *)
|
|||
|
||||
if (this->isVisible() && this->hasCustomWindowFrame())
|
||||
{
|
||||
auto palette = this->palette();
|
||||
palette.setColor(QPalette::Window,
|
||||
GetForegroundWindow() == HWND(this->winId())
|
||||
? QColor(90, 90, 90)
|
||||
: QColor(50, 50, 50));
|
||||
this->setPalette(palette);
|
||||
auto hwnd = this->safeHWND();
|
||||
if (hwnd)
|
||||
{
|
||||
auto palette = this->palette();
|
||||
palette.setColor(QPalette::Window, GetForegroundWindow() == *hwnd
|
||||
? QColor(90, 90, 90)
|
||||
: QColor(50, 50, 50));
|
||||
this->setPalette(palette);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -495,14 +531,29 @@ void BaseWindow::leaveEvent(QEvent *)
|
|||
|
||||
void BaseWindow::moveTo(QPoint point, widgets::BoundsChecking mode)
|
||||
{
|
||||
this->lastBoundsCheckPosition_ = point;
|
||||
this->lastBoundsCheckMode_ = mode;
|
||||
widgets::moveWindowTo(this, point, mode);
|
||||
}
|
||||
|
||||
void BaseWindow::showAndMoveTo(QPoint point, widgets::BoundsChecking mode)
|
||||
{
|
||||
this->lastBoundsCheckPosition_ = point;
|
||||
this->lastBoundsCheckMode_ = mode;
|
||||
widgets::showAndMoveWindowTo(this, point, mode);
|
||||
}
|
||||
|
||||
bool BaseWindow::applyLastBoundsCheck()
|
||||
{
|
||||
if (this->lastBoundsCheckMode_ == widgets::BoundsChecking::Off)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this->moveTo(this->lastBoundsCheckPosition_, this->lastBoundsCheckMode_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BaseWindow::resizeEvent(QResizeEvent *)
|
||||
{
|
||||
// Queue up save because: Window resized
|
||||
|
@ -516,14 +567,18 @@ void BaseWindow::resizeEvent(QResizeEvent *)
|
|||
{
|
||||
this->isResizeFixing_ = true;
|
||||
QTimer::singleShot(50, this, [this] {
|
||||
auto hwnd = this->safeHWND();
|
||||
if (!hwnd)
|
||||
{
|
||||
this->isResizeFixing_ = false;
|
||||
return;
|
||||
}
|
||||
RECT rect;
|
||||
::GetWindowRect((HWND)this->winId(), &rect);
|
||||
::SetWindowPos((HWND)this->winId(), nullptr, 0, 0,
|
||||
rect.right - rect.left + 1, rect.bottom - rect.top,
|
||||
SWP_NOMOVE | SWP_NOZORDER);
|
||||
::SetWindowPos((HWND)this->winId(), nullptr, 0, 0,
|
||||
rect.right - rect.left, rect.bottom - rect.top,
|
||||
SWP_NOMOVE | SWP_NOZORDER);
|
||||
::GetWindowRect(*hwnd, &rect);
|
||||
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1,
|
||||
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
|
||||
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left,
|
||||
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
|
||||
QTimer::singleShot(10, this, [this] {
|
||||
this->isResizeFixing_ = false;
|
||||
});
|
||||
|
@ -559,6 +614,16 @@ void BaseWindow::showEvent(QShowEvent *)
|
|||
{
|
||||
this->moveTo(this->pos(), widgets::BoundsChecking::CursorPosition);
|
||||
}
|
||||
|
||||
if (!this->flags_.has(TopMost))
|
||||
{
|
||||
QTimer::singleShot(1, this, [this] {
|
||||
if (!this->waitingForTopMost_)
|
||||
{
|
||||
this->tryApplyTopMost();
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -628,7 +693,7 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
|
|||
long y = GET_Y_LPARAM(msg->lParam);
|
||||
|
||||
RECT winrect;
|
||||
GetWindowRect(HWND(winId()), &winrect);
|
||||
GetWindowRect(msg->hwnd, &winrect);
|
||||
QPoint globalPos(x, y);
|
||||
this->ui_.titlebarButtons->hover(msg->wParam, globalPos);
|
||||
this->lastEventWasNcMouseMove_ = true;
|
||||
|
@ -679,7 +744,7 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
|
|||
long y = GET_Y_LPARAM(msg->lParam);
|
||||
|
||||
RECT winrect;
|
||||
GetWindowRect(HWND(winId()), &winrect);
|
||||
GetWindowRect(msg->hwnd, &winrect);
|
||||
QPoint globalPos(x, y);
|
||||
if (msg->message == WM_NCLBUTTONDOWN)
|
||||
{
|
||||
|
@ -827,7 +892,7 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
|
|||
{
|
||||
// disable OS window border
|
||||
const MARGINS margins = {-1};
|
||||
DwmExtendFrameIntoClientArea(HWND(this->winId()), &margins);
|
||||
DwmExtendFrameIntoClientArea(msg->hwnd, &margins);
|
||||
}
|
||||
|
||||
if (!this->initalBounds_.isNull())
|
||||
|
@ -890,8 +955,8 @@ bool BaseWindow::handleSIZE(MSG *msg)
|
|||
{
|
||||
if (msg->wParam == SIZE_MAXIMIZED)
|
||||
{
|
||||
auto offset = int(
|
||||
getWindowDpi(HWND(this->winId())).value_or(96) * 8 / 96);
|
||||
auto offset =
|
||||
int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96);
|
||||
|
||||
this->ui_.windowLayout->setContentsMargins(offset, offset,
|
||||
offset, offset);
|
||||
|
@ -912,6 +977,13 @@ bool BaseWindow::handleSIZE(MSG *msg)
|
|||
QPoint(rect.right - 1, rect.bottom - 1));
|
||||
}
|
||||
this->useNextBounds_.stop();
|
||||
|
||||
if (msg->wParam == SIZE_MINIMIZED && this->ui_.titlebarButtons)
|
||||
{
|
||||
// Windows doesn't send a WM_NCMOUSELEAVE event when clicking
|
||||
// the minimize button, so we have to emulate it.
|
||||
this->ui_.titlebarButtons->leave();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -945,7 +1017,7 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
|
|||
#ifdef USEWINSDK
|
||||
const LONG border_width = 8; // in pixels
|
||||
RECT winrect;
|
||||
GetWindowRect(HWND(winId()), &winrect);
|
||||
GetWindowRect(msg->hwnd, &winrect);
|
||||
|
||||
long x = GET_X_LPARAM(msg->lParam);
|
||||
long y = GET_Y_LPARAM(msg->lParam);
|
||||
|
@ -1124,4 +1196,15 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
|
|||
#endif
|
||||
}
|
||||
|
||||
#ifdef USEWINSDK
|
||||
std::optional<HWND> BaseWindow::safeHWND() const
|
||||
{
|
||||
if (!this->testAttribute(Qt::WA_WState_Created))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
return reinterpret_cast<HWND>(this->winId());
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -65,13 +65,32 @@ public:
|
|||
**/
|
||||
void showAndMoveTo(QPoint point, widgets::BoundsChecking mode);
|
||||
|
||||
/// @brief Applies the last moveTo operation if that one was bounds-checked
|
||||
///
|
||||
/// If there was a previous moveTo or showAndMoveTo operation with a mode
|
||||
/// other than `Off`, a moveTo is repeated with the last supplied @a point
|
||||
/// and @a mode. Note that in the case of showAndMoveTo, moveTo is run.
|
||||
///
|
||||
/// @returns true if there was a previous bounds-checked moveTo operation
|
||||
bool applyLastBoundsCheck();
|
||||
|
||||
float scale() const override;
|
||||
float qtFontScale() const;
|
||||
|
||||
/// @returns true if the window is the top-most window.
|
||||
/// Either #setTopMost was called or the `TopMost` flag is set which overrides this
|
||||
bool isTopMost() const;
|
||||
/// Updates the window's top-most status
|
||||
/// If the `TopMost` flag is set, this is a no-op
|
||||
void setTopMost(bool topMost);
|
||||
|
||||
pajlada::Signals::NoArgSignal closing;
|
||||
|
||||
static bool supportsCustomWindowFrame();
|
||||
|
||||
signals:
|
||||
void topMostChanged(bool topMost);
|
||||
|
||||
protected:
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
bool nativeEvent(const QByteArray &eventType, void *message,
|
||||
|
@ -131,6 +150,7 @@ private:
|
|||
FlagsEnum<Flags> flags_;
|
||||
float nativeScale_ = 1;
|
||||
bool isResizeFixing_ = false;
|
||||
bool isTopMost_ = false;
|
||||
|
||||
struct {
|
||||
QLayout *windowLayout = nullptr;
|
||||
|
@ -141,7 +161,31 @@ private:
|
|||
std::vector<Button *> buttons;
|
||||
} ui_;
|
||||
|
||||
/// The last @a pos from moveTo and showAndMoveTo
|
||||
QPoint lastBoundsCheckPosition_;
|
||||
/// The last @a mode from moveTo and showAndMoveTo
|
||||
widgets::BoundsChecking lastBoundsCheckMode_ = widgets::BoundsChecking::Off;
|
||||
|
||||
#ifdef USEWINSDK
|
||||
/// @brief Returns the HWND of this window if it has one
|
||||
///
|
||||
/// A QWidget only has an HWND if it has been created. Before that,
|
||||
/// accessing `winID()` will create the window which can lead to unintended
|
||||
/// bugs.
|
||||
std::optional<HWND> safeHWND() const;
|
||||
|
||||
/// @brief Tries to apply the `isTopMost_` setting
|
||||
///
|
||||
/// If the setting couldn't be applied (because the window wasn't created
|
||||
/// yet), the operation is repeated after a short delay.
|
||||
///
|
||||
/// @pre When calling from outside this method, `waitingForTopMost_` must
|
||||
/// be `false` to avoid too many pending calls.
|
||||
/// @post If an operation was queued to be executed after some delay,
|
||||
/// `waitingForTopMost_` will be set to `true`.
|
||||
void tryApplyTopMost();
|
||||
bool waitingForTopMost_ = false;
|
||||
|
||||
QRect initalBounds_;
|
||||
QRect currentBounds_;
|
||||
QRect nextBounds_;
|
||||
|
|
|
@ -59,6 +59,28 @@ Notebook::Notebook(QWidget *parent)
|
|||
});
|
||||
this->updateTabVisibilityMenuAction();
|
||||
|
||||
this->toggleTopMostAction_ = new QAction("Top most window", this);
|
||||
this->toggleTopMostAction_->setCheckable(true);
|
||||
auto *window = dynamic_cast<BaseWindow *>(this->window());
|
||||
if (window)
|
||||
{
|
||||
auto updateTopMost = [this, window] {
|
||||
this->toggleTopMostAction_->setChecked(window->isTopMost());
|
||||
};
|
||||
updateTopMost();
|
||||
QObject::connect(this->toggleTopMostAction_, &QAction::triggered,
|
||||
window, [window] {
|
||||
window->setTopMost(!window->isTopMost());
|
||||
});
|
||||
QObject::connect(window, &BaseWindow::topMostChanged, this,
|
||||
updateTopMost);
|
||||
}
|
||||
else
|
||||
{
|
||||
qCWarning(chatterinoApp)
|
||||
<< "Notebook must be created within a BaseWindow";
|
||||
}
|
||||
|
||||
this->addNotebookActionsToMenu(&this->menu_);
|
||||
|
||||
// Manually resize the add button so the initial paint uses the correct
|
||||
|
@ -1181,6 +1203,8 @@ void Notebook::addNotebookActionsToMenu(QMenu *menu)
|
|||
menu->addAction(this->showTabsAction_);
|
||||
|
||||
menu->addAction(this->lockNotebookLayoutAction_);
|
||||
|
||||
menu->addAction(this->toggleTopMostAction_);
|
||||
}
|
||||
|
||||
NotebookButton *Notebook::getAddButton()
|
||||
|
|
|
@ -196,6 +196,7 @@ private:
|
|||
NotebookTabLocation tabLocation_ = NotebookTabLocation::Top;
|
||||
QAction *lockNotebookLayoutAction_;
|
||||
QAction *showTabsAction_;
|
||||
QAction *toggleTopMostAction_;
|
||||
|
||||
// This filter, if set, is used to figure out the visibility of
|
||||
// the tabs in this notebook.
|
||||
|
@ -224,7 +225,6 @@ private:
|
|||
|
||||
// Main window on Windows has basically a duplicate of this in Window
|
||||
NotebookButton *streamerModeIcon_{};
|
||||
|
||||
void updateStreamerModeIcon();
|
||||
};
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ TooltipWidget::TooltipWidget(BaseWidget *parent)
|
|||
if (needSizeAdjustment)
|
||||
{
|
||||
this->adjustSize();
|
||||
this->applyLastBoundsCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,7 +19,8 @@
|
|||
#include "messages/MessageElement.hpp"
|
||||
#include "messages/MessageThread.hpp"
|
||||
#include "providers/colors/ColorProvider.hpp"
|
||||
#include "providers/LinkResolver.hpp"
|
||||
#include "providers/links/LinkInfo.hpp"
|
||||
#include "providers/links/LinkResolver.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
@ -45,6 +46,7 @@
|
|||
#include "widgets/TooltipWidget.hpp"
|
||||
#include "widgets/Window.hpp"
|
||||
|
||||
#include <magic_enum/magic_enum_flags.hpp>
|
||||
#include <QClipboard>
|
||||
#include <QColor>
|
||||
#include <QDate>
|
||||
|
@ -52,6 +54,7 @@
|
|||
#include <QDesktopServices>
|
||||
#include <QEasingCurve>
|
||||
#include <QGraphicsBlurEffect>
|
||||
#include <QJsonDocument>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
|
@ -244,6 +247,30 @@ void addHiddenContextMenuItems(QMenu *menu,
|
|||
crossPlatformCopy(messageID);
|
||||
});
|
||||
}
|
||||
|
||||
const auto *message = layout->getMessage();
|
||||
|
||||
if (message != nullptr)
|
||||
{
|
||||
QJsonDocument jsonDocument;
|
||||
|
||||
QJsonObject jsonObject;
|
||||
|
||||
jsonObject["id"] = message->id;
|
||||
jsonObject["searchText"] = message->searchText;
|
||||
jsonObject["messageText"] = message->messageText;
|
||||
jsonObject["flags"] = QString::fromStdString(
|
||||
magic_enum::enum_flags_name(message->flags.value()));
|
||||
|
||||
jsonDocument.setObject(jsonObject);
|
||||
|
||||
auto jsonString =
|
||||
jsonDocument.toJson(QJsonDocument::JsonFormat::Indented);
|
||||
|
||||
menu->addAction("Copy message &JSON", [jsonString] {
|
||||
crossPlatformCopy(jsonString);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Current function: https://www.desmos.com/calculator/vdyamchjwh
|
||||
|
@ -1911,55 +1938,16 @@ void ChannelView::mouseMoveEvent(QMouseEvent *event)
|
|||
}
|
||||
else
|
||||
{
|
||||
if (element->getTooltip() == "No link info loaded")
|
||||
{
|
||||
std::weak_ptr<MessageLayout> weakLayout = layout;
|
||||
LinkResolver::getLinkInfo(
|
||||
element->getLink().value, nullptr,
|
||||
[weakLayout, element](QString tooltipText,
|
||||
Link originalLink,
|
||||
ImagePtr thumbnail) {
|
||||
auto shared = weakLayout.lock();
|
||||
if (!shared)
|
||||
{
|
||||
return;
|
||||
}
|
||||
element->setTooltip(tooltipText);
|
||||
element->setThumbnail(thumbnail);
|
||||
});
|
||||
}
|
||||
auto thumbnailSize = getSettings()->thumbnailSize;
|
||||
if (thumbnailSize == 0)
|
||||
auto *linkElement = dynamic_cast<LinkElement *>(element);
|
||||
if (linkElement)
|
||||
{
|
||||
// "Show thumbnails" is set to "Off", show text only
|
||||
this->tooltipWidget_->setOne({nullptr, element->getTooltip()});
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto shouldHideThumbnail =
|
||||
isInStreamerMode() &&
|
||||
getSettings()->streamerModeHideLinkThumbnails &&
|
||||
element->getThumbnail() != nullptr &&
|
||||
!element->getThumbnail()->url().string.isEmpty();
|
||||
auto thumb =
|
||||
shouldHideThumbnail
|
||||
? Image::fromResourcePixmap(getResources().streamerMode)
|
||||
: element->getThumbnail();
|
||||
|
||||
if (element->getThumbnailType() ==
|
||||
MessageElement::ThumbnailType::Link_Thumbnail)
|
||||
if (linkElement->linkInfo()->isPending())
|
||||
{
|
||||
this->tooltipWidget_->setOne({
|
||||
std::move(thumb),
|
||||
element->getTooltip(),
|
||||
thumbnailSize,
|
||||
thumbnailSize,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->tooltipWidget_->setOne({std::move(thumb), ""});
|
||||
getIApp()->getLinkResolver()->resolve(
|
||||
linkElement->linkInfo());
|
||||
}
|
||||
this->setLinkInfoTooltip(linkElement->linkInfo());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3077,4 +3065,63 @@ bool ChannelView::canReplyToMessages() const
|
|||
return true;
|
||||
}
|
||||
|
||||
void ChannelView::setLinkInfoTooltip(LinkInfo *info)
|
||||
{
|
||||
assert(info);
|
||||
|
||||
auto thumbnailSize = getSettings()->thumbnailSize;
|
||||
|
||||
ImagePtr thumbnail;
|
||||
if (info->hasThumbnail() && thumbnailSize > 0)
|
||||
{
|
||||
if (isInStreamerMode() && getSettings()->streamerModeHideLinkThumbnails)
|
||||
{
|
||||
thumbnail = Image::fromResourcePixmap(getResources().streamerMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
thumbnail = info->thumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
this->tooltipWidget_->setOne({
|
||||
.image = thumbnail,
|
||||
.text = info->tooltip(),
|
||||
.customWidth = thumbnailSize,
|
||||
.customHeight = thumbnailSize,
|
||||
});
|
||||
|
||||
if (info->isLoaded())
|
||||
{
|
||||
this->pendingLinkInfo_.clear();
|
||||
return; // Either resolved or errored (can't change anymore)
|
||||
}
|
||||
|
||||
// listen to changes
|
||||
|
||||
if (this->pendingLinkInfo_.data() == info)
|
||||
{
|
||||
return; // same info - already registered
|
||||
}
|
||||
|
||||
if (this->pendingLinkInfo_)
|
||||
{
|
||||
QObject::disconnect(this->pendingLinkInfo_.data(),
|
||||
&LinkInfo::stateChanged, this, nullptr);
|
||||
}
|
||||
QObject::connect(info, &LinkInfo::stateChanged, this,
|
||||
&ChannelView::pendingLinkInfoStateChanged);
|
||||
this->pendingLinkInfo_ = info;
|
||||
}
|
||||
|
||||
void ChannelView::pendingLinkInfoStateChanged()
|
||||
{
|
||||
if (!this->pendingLinkInfo_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->setLinkInfoTooltip(this->pendingLinkInfo_.data());
|
||||
this->tooltipWidget_->applyLastBoundsCheck();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QMenu>
|
||||
#include <QPaintEvent>
|
||||
#include <QPointer>
|
||||
#include <QScroller>
|
||||
#include <QTimer>
|
||||
#include <QVariantAnimation>
|
||||
|
@ -47,6 +48,8 @@ class Split;
|
|||
class FilterSet;
|
||||
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
||||
|
||||
class LinkInfo;
|
||||
|
||||
enum class PauseReason {
|
||||
Mouse,
|
||||
Selection,
|
||||
|
@ -366,6 +369,17 @@ private:
|
|||
void scrollUpdateRequested();
|
||||
|
||||
TooltipWidget *const tooltipWidget_{};
|
||||
|
||||
/// Pointer to a link info that hasn't loaded yet
|
||||
QPointer<LinkInfo> pendingLinkInfo_;
|
||||
|
||||
/// @brief Sets the tooltip to contain the link info
|
||||
///
|
||||
/// If the info isn't loaded yet, it's tracked until it's resolved or errored.
|
||||
void setLinkInfoTooltip(LinkInfo *info);
|
||||
|
||||
/// Slot for the LinkInfo::stateChanged signal.
|
||||
void pendingLinkInfoStateChanged();
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -327,6 +327,18 @@ void NotebookTab::setTabLocation(NotebookTabLocation location)
|
|||
}
|
||||
}
|
||||
|
||||
bool NotebookTab::setRerun(bool isRerun)
|
||||
{
|
||||
if (this->isRerun_ != isRerun)
|
||||
{
|
||||
this->isRerun_ = isRerun;
|
||||
this->update();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NotebookTab::setLive(bool isLive)
|
||||
{
|
||||
if (this->isLive_ != isLive)
|
||||
|
@ -514,12 +526,22 @@ void NotebookTab::paintEvent(QPaintEvent *)
|
|||
painter.fillRect(lineRect, lineColor);
|
||||
|
||||
// draw live indicator
|
||||
if (this->isLive_ && getSettings()->showTabLive)
|
||||
if ((this->isLive_ || this->isRerun_) && getSettings()->showTabLive)
|
||||
{
|
||||
painter.setPen(QColor(Qt::GlobalColor::red));
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
// Live overrides rerun
|
||||
QBrush b;
|
||||
b.setColor(QColor(Qt::GlobalColor::red));
|
||||
if (this->isLive_)
|
||||
{
|
||||
painter.setPen(this->theme->tabs.liveIndicator);
|
||||
b.setColor(this->theme->tabs.liveIndicator);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.setPen(this->theme->tabs.rerunIndicator);
|
||||
b.setColor(this->theme->tabs.rerunIndicator);
|
||||
}
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
b.setStyle(Qt::SolidPattern);
|
||||
painter.setBrush(b);
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue