diff --git a/.CI/CreateAppImage.sh b/.CI/CreateAppImage.sh old mode 100644 new mode 100755 index 1ca0f67a..6d533444 --- a/.CI/CreateAppImage.sh +++ b/.CI/CreateAppImage.sh @@ -1,21 +1,45 @@ -export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/qt512/lib/ -ldd ./bin/chatterino -make INSTALL_ROOT=appdir -j$(nproc) install ; find appdir/ -cp ./resources/icon.png ./appdir/chatterino.png +#!/bin/sh -wget -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" -chmod a+x linuxdeployqt-continuous-x86_64.AppImage -wget -nv "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -chmod a+x appimagetool-x86_64.AppImage -./linuxdeployqt-continuous-x86_64.AppImage \ +set -e + +if [ ! -f ./bin/chatterino ] || [ ! -x ./bin/chatterino ]; then + echo "ERROR: No chatterino binary file found. This script must be run in the build folder, and chatterino must be built first." + exit 1 +fi + +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/opt/qt512/lib/" +export PATH="/opt/qt512/bin:$PATH" + +script_path=$(readlink -f "$0") +script_dir=$(dirname "$script_path") +chatterino_dir=$(dirname "$script_dir") + +qmake_path=$(command -v qmake) + +ldd ./bin/chatterino +make INSTALL_ROOT=appdir -j"$(nproc)" install ; find appdir/ +cp "$chatterino_dir"/resources/icon.png ./appdir/chatterino.png + +linuxdeployqt_path="linuxdeployqt-6-x86_64.AppImage" +linuxdeployqt_url="https://github.com/probonopd/linuxdeployqt/releases/download/6/linuxdeployqt-6-x86_64.AppImage" + +if [ ! -f "$linuxdeployqt_path" ]; then + wget -nv "$linuxdeployqt_url" + chmod a+x "$linuxdeployqt_path" +fi +if [ ! -f appimagetool-x86_64.AppImage ]; then + wget -nv "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod a+x appimagetool-x86_64.AppImage +fi +./"$linuxdeployqt_path" \ appdir/usr/share/applications/*.desktop \ -no-translations \ -bundle-non-qt-libs \ -unsupported-allow-new-glibc \ - -qmake=/opt/qt512/bin/qmake + -qmake="$qmake_path" rm -rf appdir/home -rm appdir/AppRun +rm -f appdir/AppRun # shellcheck disable=SC2016 echo '#!/bin/sh diff --git a/.CI/CreateDMG.sh b/.CI/CreateDMG.sh new file mode 100755 index 00000000..60a8f534 --- /dev/null +++ b/.CI/CreateDMG.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +echo "Running MACDEPLOYQT" +/usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg +echo "Creating APP folder" +mkdir app +echo "Running hdiutil attach on the built DMG" +hdiutil attach chatterino.dmg +echo "Copying chatterino.app into the app folder" +cp -r /Volumes/chatterino/chatterino.app app/ +echo "Creating DMG with create-dmg" +create-dmg --volname Chatterino2 --volicon ../resources/chatterino.icns --icon-size 50 --app-drop-link 0 0 --format UDBZ chatterino-osx.dmg app/ +echo "DONE" diff --git a/.CI/InstallQTStylePlugins.sh b/.CI/InstallQTStylePlugins.sh old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1d4bbedf..a466c17b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,17 +10,17 @@ assignees: '' **Describe the bug** -**To Reproduce** +**To reproduce** -**Expected behavior** - - **Screenshots** **Chatterino version** - + -**Additional context** - +**Operating system** + + +**Additional information** + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..eea852f4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,207 @@ +--- +name: Build + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + pull_request: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest, macos-latest] + fail-fast: false + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - name: Install Qt + uses: jurplel/install-qt-action@v2 + with: + modules: qtwebengine + + # WINDOWS + - name: Install dependencies (Windows) + if: startsWith(matrix.os, 'windows') + run: | + REM We use this source (temporarily?) until choco has updated their version of conan + choco source add -n=AFG -s="https://api.bintray.com/nuget/anotherfoxguy/choco-pkg" + choco install conan -y + + refreshenv + shell: cmd + + - name: Build (Windows) + if: startsWith(matrix.os, 'windows') + run: | + call "%programfiles(x86)%\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + mkdir build + cd build + "C:\Program Files\Conan\conan\conan.exe" install .. + qmake .. + set cl=/MP + nmake /S /NOLOGO + windeployqt release/chatterino.exe --release --no-compiler-runtime --no-translations --no-opengl-sw --dir Chatterino2/ + cp release/chatterino.exe Chatterino2/ + echo nightly > Chatterino2/modes + 7z a chatterino-windows-x86-64.zip Chatterino2/ + shell: cmd + + - name: Upload artifact (Windows) + if: startsWith(matrix.os, 'windows') + uses: actions/upload-artifact@v1 + with: + name: chatterino-windows-x86-64.zip + path: build/chatterino-windows-x86-64.zip + + # LINUX + - name: Install dependencies (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: sudo apt-get update && sudo apt-get -y install libssl-dev libboost-dev libboost-system-dev libboost-filesystem-dev libpulse-dev libxkbcommon-x11-0 libgstreamer-plugins-base1.0-0 + + - name: Build (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + mkdir build + cd build + qmake PREFIX=/usr .. + make -j8 + shell: bash + + - name: Package (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + cd build + sh ./../.CI/CreateAppImage.sh + shell: bash + + - name: Upload artifact (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + uses: actions/upload-artifact@v1 + with: + name: Chatterino-x86_64.AppImage + path: build/Chatterino-x86_64.AppImage + + # MACOS + - name: Install dependencies (MacOS) + if: startsWith(matrix.os, 'macos') + run: | + brew install boost openssl rapidjson qt p7zip create-dmg + shell: bash + + - name: Build (MacOS) + if: startsWith(matrix.os, 'macos') + run: | + mkdir build + cd build + /usr/local/opt/qt/bin/qmake .. DEFINES+=$dateOfBuild + sed -ie 's/-framework\\\ /-framework /g' Makefile + make -j8 + shell: bash + + - name: Package (MacOS) + if: startsWith(matrix.os, 'macos') + run: | + ls -la + pwd + ls -la build || true + cd build + sh ./../.CI/CreateDMG.sh + shell: bash + + - name: Upload artifact (MacOS) + if: startsWith(matrix.os, 'macos') + uses: actions/upload-artifact@v1 + with: + name: chatterino-osx.dmg + path: build/chatterino-osx.dmg + + create-release: + needs: build + runs-on: ubuntu-latest + if: (github.event_name == 'push' && github.ref == 'refs/heads/master') + + steps: + - name: Create release + id: create_release + uses: pajlada/create-release@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: github-actions-nightly + backup_tag_name: backup-github-actions-nightly + release_name: GitHub Actions Nightly Test + body: | + 1 ${{ github.eventName }} + 2 ${{ github.sha }} + 3 ${{ github.ref }} + 4 ${{ github.workflow }} + 5 ${{ github.action }} + 6 ${{ github.actor }} + prerelease: true + + - uses: actions/download-artifact@v1 + with: + name: chatterino-windows-x86-64.zip + path: windows/ + + - uses: actions/download-artifact@v1 + with: + name: Chatterino-x86_64.AppImage + path: linux/ + + - uses: actions/download-artifact@v1 + with: + name: chatterino-osx.dmg + path: macos/ + + # TODO: Extract dmg and appimage + + - name: TREE + run: | + sudo apt update && sudo apt install tree + tree . + + # - name: Read upload URL into output + # id: upload_url + # run: | + # echo "::set-output name=upload_url::$(cat release-upload-url.txt/release-upload-url.txt)" + + - name: Upload release asset (Windows) + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./windows/chatterino-windows-x86-64.zip + asset_name: chatterino-windows-x86-64.zip + asset_content_type: application/zip + + - name: Upload release asset (Ubuntu) + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./linux/Chatterino-x86_64.AppImage + asset_name: Chatterino-x86_64.AppImage + asset_content_type: application/x-executable + + - name: Upload release asset (MacOS) + uses: actions/upload-release-asset@v1.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./macos/chatterino-osx.dmg + asset_name: chatterino-osx.dmg + asset_content_type: application/x-bzip2 diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml new file mode 100644 index 00000000..3c04502b --- /dev/null +++ b/.github/workflows/check-formatting.yml @@ -0,0 +1,20 @@ +name: Check formatting + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + container: + image: ubuntu:19.10 + + steps: + - uses: actions/checkout@v1 + - name: apt-get update + run: apt-get update + + - name: Install clang-format + run: apt-get -y install clang-format + + - name: Check formatting + run: ./tools/check-format.sh diff --git a/.gitmodules b/.gitmodules index 5b6b0960..8ac27af1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "lib/libcommuni"] path = lib/libcommuni - url = https://github.com/hemirt/libcommuni + url = https://github.com/Chatterino/libcommuni [submodule "lib/humanize"] path = lib/humanize url = https://github.com/pajlada/humanize.git diff --git a/.travis.yml b/.travis.yml index 6387df0a..5e6041b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,7 +64,9 @@ matrix: script: - mkdir build && cd build - dateOfBuild="CHATTERINO_NIGHTLY_VERSION_STRING=\"\\\"$(date +%d.%m.%Y)\\\"\"" - - /usr/local/opt/qt/bin/qmake .. DEFINES+=$dateOfBuild && make -j8 + - /usr/local/opt/qt/bin/qmake .. DEFINES+=$dateOfBuild + - sed -ie 's/-framework\\\ /-framework /g' Makefile + - make -j8 - /usr/local/opt/qt/bin/macdeployqt chatterino.app -dmg - mkdir app - hdiutil attach chatterino.dmg diff --git a/BUILDING_ON_LINUX.md b/BUILDING_ON_LINUX.md index 0c717d8b..0451e559 100644 --- a/BUILDING_ON_LINUX.md +++ b/BUILDING_ON_LINUX.md @@ -14,10 +14,16 @@ install [chatterino2-git](https://aur.archlinux.org/packages/chatterino2-git/) f 1. create build folder `mkdir build && cd build` 1. `qmake .. && make` -## Fedora 28 -*most likely works the same for other Red Hat-like distros* -1. `sudo yum install qt-creator qt5-qtmultimedia-devel qt5-qtsvg-devel openssl-devel gstreamer-plugins-ugly gstreamer-plugins-good boost-devel rapidjson-devel` -1. Open `chatterino.pro` with QT Creator and build +## Fedora 28 and above +*most likely works the same for other Red Hat-like distros. Substitue `dnf` with `yum`.* +### Development dependencies +1. `sudo dnf install qt5-qtbase-devel qt5-qtmultimedia-devel qt5-qtsvg-devel libsecret-devel openssl-devel boost-devel` +1. go into project directory +1. create build folder `mkdir build && cd build` +1. `qmake-qt5 .. && make -j$(nproc)` +### Optional dependencies +*`gstreamer-plugins-good` package is retired in Fedora 31, see: [rhbz#1735324](https://bugzilla.redhat.com/show_bug.cgi?id=1735324)* +1. `sudo dnf install gstreamer-plugins-good` *(optional: for audio output)* ## NixOS 18.09+ 1. enter the development environment with all of the dependencies: `nix-shell -p openssl boost qt5.full` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..0e4c00ac --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,220 @@ +# Chatterino code guidelines + +This is a set of guidelines for contributing to Chatterino. The goal is to teach programmers without C++ background (java/python/etc.), people who haven't used Qt or otherwise have different experience the idioms of the codebase. Thus we will focus on those which are different from those other environments. There are extra guidelines available [here](https://hackmd.io/@fourtf/chatterino-pendantic-guidelines) but they are considered as extras and not as important. + +# Tooling + +Formatting +------ + +Code is automatically formatted using `clang-format`. It takes the burden off of the programmer and ensures that all contributors use the same style (even if mess something up accidentally). We recommend that you set up automatic formatting on file save in your editor. + +# Comments + +Comments should only be used to: + +- Increase readability (e.g. grouping member variables). +- Containing information that can't be expressed in code. + +Try to structure your code so that comments are not required. + +#### Good example + +``` cpp +/// Result is 0 if a == b, negative if a < b and positive if b > a. +/// ^^^ You can't know this from the function signature! +// Even better: Return a "strong ordering" type. +// (but we don't have such a type right now) +int compare(const QString &a, const QString &b); +``` + +#### Bad example + +``` cpp +/* + * Matches a link and returns boost::none if it failed and a + * QRegularExpressionMatch on success. + * ^^^ This comment just repeats the function signature!!! + * + * @param text The text that will be checked if it contains a + * link + * ^^^ No need to repeat the obvious. + */ +boost::optional matchLink(const QString &text); +``` + + +# Code + +Arithmetic Types +----- + +Arithmetic types (like char, short, int, long, float and double), bool, and pointers are NOT initialized by default in c++. They keep whatever value is already at their position in the memory. This makes debugging harder and is unpredictable, so we initialize them to zero by using `{}` after their name when declaring them. + +``` cpp +class ArithmeticTypes +{ + int thisIs0{}; + QWidget *thisIsNull{}; + bool thisIsFalse_{}; + // int a; // <- Initialized to "random" value. + // QWidget *randomPtr. + + std::vector myVec; // <- other types call constructors instead, so no need for {} + // std::vector myVec{}; <- pointless {} + + int thisIs5 = 5; // <- Also fine, we initialize it with another value. +}; + +void myFunc() { + int a = 1 + 1; // <- here we initialize it immediately, so it's fine. +} +``` + +Passing parameters +------ +The way a parameter is passed signals how it is going to be used inside of the function. C++ doesn't have multiple return values so there is "out parameters" (reference to a variable that is going to be assigned inside of the function) to simulate multiple return values. + +**Cheap to copy types** like int/enum/etc. can be passed in per value since copying them is fast. +``` cpp +void setValue(int value) { + // ... +} +``` + +**References** mean that the variable doesn't need to be copied when it is passed to a function. + +|type|meaning| +|-|-| +|`const Type& name`|*in* Parameter. It is NOT going to be modified and may be copied inside of the function.| +|`Type& name`|*out* or *in+out* Parmameter. It will be modified.| + +**Pointers** signal that objects are managed manually. While the above are only guaranteed to live as long as the function call (= don't store and use later) these may have more complex lifetimes. + +|type|meaning| +|-|-| +|`Type* name`|The lifetime of the parameter may exceed the length of the function call. It may use the `QObject` parent/children system.| + +**R-value references** `&&` work similar to regular references but signal the parameter should be "consumed". + +``` cpp +void storeLargeObject(LargeObject &&object) { + // ... +} + +void storeObject(std::unique_ptr &&object) { + // ... +} + +void main() { + // initialize a large object (= will be expensive to copy) + LargeObject large = // ... + + // Object accepts an r-value reference + we use std::move() + // => We move the object = no need to copy. + storeLargeObject(std::move(large)); + + // But even worse, you can't copy a unique_ptr so we need to move here! + std::unique_ptr unique = // ... + storeObject(std::move(unique)); + + // The pointer contained by unique has now been consumed by "storeObject" + // so it just holds a null pointer now. + assert(unique.get() == nullptr); +} +``` + +Generally the lowest level of requirement should be used e.g. passing `Channel&` instead of `std::shared_ptr&` (aka `ChannelPtr`) if possible. + + +Members +----- + +All functions names are in `camelCase`. *Private* member variables are in `camelCase_` (note the underscore at the end). We don't use the `get` prefix for getters. We mark functions as `const` [if applicable](https://stackoverflow.com/questions/751681/meaning-of-const-last-in-a-function-declaration-of-a-class). +``` cpp +class NamedObject +{ +public: + const QString &name() const; // <- no "get" prefix. + void setName(const QString &name); + bool hasLongName() const; // <- "has" or "is" prefix is okay + + static void myStaticFunction(); // <- also lowercase + QString publicName; + +private: + // Private variables have "_" suffix. + QString name_; + // QString name; <- collides with name() function +}; + +void myFreeStandingFunction(); // <- also lower case +``` + +Casts +------ + +- **Avoid** c-style casts: `(type)variable`. +- Instead use explicit type casts: `type(variable)` +- Or use one of [static_cast](https://en.cppreference.com/w/cpp/language/static_cast), [const_cast](https://en.cppreference.com/w/cpp/language/const_cast) and [dynamic_cast](https://en.cppreference.com/w/cpp/language/dynamic_cast) +- Try to avoid [reinterpret_cast](https://en.cppreference.com/w/cpp/language/reinterpret_cast) unless necessary. + +``` cpp +void example() { + float f = 123.456; + int i = (int)f; // <- don't + int i = int(f); // <- do + + Base* base = // ... + Derived* derived = (Derived*)base; // <- don't + Derived* derived = dynamic_cast(base); // <- do + + // Only use "const_cast" solved if using proper const correctness doesn't work. + const int c = 123; + ((int &)c) = 123; // <- don't + const_cast(c) = 123; // <- do (but only sometimes) + + // "reinterpret_cast" is also only required in very rarely. + int p = 123; + float *pp = (float*)&p; + float *pp = reinterpret_cast(&p); +} +``` + + +This +------ +Always use `this` to refer to instance members to make it clear where we use either locals or members. + +``` cpp +class Test +{ + void testFunc(int a); + int testInt_{}; +} + +Test::testFunc(int a) +{ + // do + this->testInt_ += 2; + this->testFunc(); + + // don't + testInt_ -= 123; + testFunc(2); + this->testFunc(testInt_ + 1); +} +``` + +Managing resources +------ + +#### Regular classes +Keep the element on the stack if possible. If you need a pointer or have complex ownership you should use one of these classes: +- Use `std::unique_ptr` if the resource has a single owner. +- Use `std::shared_ptr` if the resource has multiple owners. + +#### QObject classes +- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetime where possible. Objects are destroyed when their parent object is destroyed. +- If you have to explicitly delete an object use `variable->deleteLater()` instead of `delete variable`. This ensures that it will be deleted on the correct thread. +- If an object doesn't have a parent consider using `std::unique_ptr` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope or the object is destroyed. diff --git a/README.md b/README.md index a01b015e..519fa4da 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ git submodule update --init --recursive [Building on Mac](../master/BUILDING_ON_MAC.md) ## Code style -The code is formatted using clang format in Qt Creator. [.clang-format](https://github.com/Chatterino/chatterino2/blob/master/.clang-format) contains the style file for clang format. +The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format. ### Get it automated with QT Creator + Beautifier + Clang Format 1. Download LLVM: http://releases.llvm.org/6.0.1/LLVM-6.0.1-win64.exe @@ -46,3 +46,5 @@ The code is formatted using clang format in Qt Creator. [.clang-format](https:// Qt creator should now format the documents when saving it. +## Doxygen +Doxygen is used to generate project information daily and is available [here](https://doxygen.chatterino.com). diff --git a/appveyor.yml b/appveyor.yml index e2b673c6..5fafd3d4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,7 +17,7 @@ install: git submodule update --init --recursive - set QTDIR=C:\Qt\5.11\msvc2017_64 + set QTDIR=C:\Qt\5.13\msvc2017_64 set PATH=%PATH%;%QTDIR%\bin diff --git a/chatterino.pro b/chatterino.pro index 169711a9..cfab8127 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -1,3 +1,16 @@ +# Exposed build flags: +# from lib/fmt.pri +# - FMT_PREFIX ($$PWD by default) +# - FMT_SYSTEM (1 = true) (Linux only, uses pkg-config) +# from lib/websocketpp.pri +# - WEBSOCKETPP_PREFIX ($$PWD by default) +# - WEBSOCKETPP_SYSTEM (1 = true) (unix only) +# from lib/rapidjson.pri +# - RAPIDJSON_PREFIX ($$PWD by default) +# - RAPIDJSON_SYSTEM (1 = true) (Linux only, uses pkg-config) +# from lib/boost.pri +# - BOOST_DIRECTORY (C:\local\boost\ by default) (Windows only) + QT += widgets core gui network multimedia svg concurrent CONFIG += communi COMMUNI += core model util @@ -8,7 +21,6 @@ TEMPLATE = app PRECOMPILED_HEADER = src/PrecompiledHeader.hpp CONFIG += precompile_header DEFINES += CHATTERINO -DEFINES += "AB_NAMESPACE=chatterino" DEFINES += AB_CUSTOM_THEME DEFINES += AB_CUSTOM_SETTINGS CONFIG += AB_NOT_STANDALONE @@ -20,12 +32,29 @@ useBreakpad { } # use C++17 +CONFIG += c++17 + +# C++17 backwards compatability win32-msvc* { QMAKE_CXXFLAGS += /std:c++17 } else { QMAKE_CXXFLAGS += -std=c++17 } +linux { + LIBS += -lrt + QMAKE_LFLAGS += -lrt + + # Enable linking libraries using PKGCONFIG += libraryname + CONFIG += link_pkgconfig +} + +macx { + INCLUDEPATH += /usr/local/include + INCLUDEPATH += /usr/local/opt/openssl/include + LIBS += -L/usr/local/opt/openssl/lib +} + # https://bugreports.qt.io/browse/QTBUG-27018 equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { TARGET = bin/chatterino @@ -39,9 +68,14 @@ macx { LIBS += -L/usr/local/lib } +# Set C_DEBUG if it's a debug build +CONFIG(debug, debug|release) { + DEFINES += C_DEBUG + DEFINES += QT_DEBUG +} + # Submodules include(lib/warnings.pri) -include(lib/appbase.pri) include(lib/fmt.pri) include(lib/humanize.pri) include(lib/libcommuni.pri) @@ -75,9 +109,13 @@ else{ SOURCES += \ src/Application.cpp \ src/autogenerated/ResourcesAutogen.cpp \ + src/BaseSettings.cpp \ + src/BaseTheme.cpp \ src/BrowserExtension.cpp \ + src/common/Args.cpp \ src/common/Channel.cpp \ src/common/ChannelChatters.cpp \ + src/common/ChatterinoSetting.cpp \ src/common/CompletionModel.cpp \ src/common/Credentials.cpp \ src/common/DownloadManager.cpp \ @@ -112,6 +150,7 @@ SOURCES += \ src/controllers/taggedusers/TaggedUser.cpp \ src/controllers/taggedusers/TaggedUsersController.cpp \ src/controllers/taggedusers/TaggedUsersModel.cpp \ + src/debug/Benchmark.cpp \ src/main.cpp \ src/messages/Emote.cpp \ src/messages/Image.cpp \ @@ -150,6 +189,7 @@ SOURCES += \ src/providers/twitch/TwitchAccount.cpp \ src/providers/twitch/TwitchAccountManager.cpp \ src/providers/twitch/TwitchApi.cpp \ + src/providers/twitch/TwitchBadge.cpp \ src/providers/twitch/TwitchBadges.cpp \ src/providers/twitch/TwitchChannel.cpp \ src/providers/twitch/TwitchEmotes.cpp \ @@ -161,6 +201,7 @@ SOURCES += \ src/RunGui.cpp \ src/singletons/Badges.cpp \ src/singletons/Emotes.cpp \ + src/singletons/Fonts.cpp \ src/singletons/helper/GifTimer.cpp \ src/singletons/helper/LoggingChannel.cpp \ src/singletons/Logging.cpp \ @@ -175,15 +216,22 @@ SOURCES += \ src/singletons/WindowManager.cpp \ src/util/DebugCount.cpp \ src/util/FormatTime.cpp \ + src/util/FunctionEventFilter.cpp \ + src/util/FuzzyConvert.cpp \ + src/util/Helpers.cpp \ src/util/IncognitoBrowser.cpp \ src/util/InitUpdateButton.cpp \ src/util/JsonQuery.cpp \ src/util/RapidjsonHelpers.cpp \ src/util/StreamLink.cpp \ src/util/NuulsUploader.cpp \ + src/util/WindowsHelper.cpp \ src/widgets/AccountSwitchPopup.cpp \ src/widgets/AccountSwitchWidget.cpp \ src/widgets/AttachedWindow.cpp \ + src/widgets/BasePopup.cpp \ + src/widgets/BaseWidget.cpp \ + src/widgets/BaseWindow.cpp \ src/widgets/dialogs/EmotePopup.cpp \ src/widgets/dialogs/IrcConnectionEditor.cpp \ src/widgets/dialogs/LastRunCrashDialog.cpp \ @@ -197,16 +245,21 @@ SOURCES += \ src/widgets/dialogs/UpdateDialog.cpp \ src/widgets/dialogs/UserInfoPopup.cpp \ src/widgets/dialogs/WelcomeDialog.cpp \ + src/widgets/helper/Button.cpp \ src/widgets/helper/ChannelView.cpp \ src/widgets/helper/ComboBoxItemDelegate.cpp \ src/widgets/helper/DebugPopup.cpp \ src/widgets/helper/EditableModelView.cpp \ + src/widgets/helper/EffectLabel.cpp \ src/widgets/helper/NotebookButton.cpp \ src/widgets/helper/NotebookTab.cpp \ src/widgets/helper/ResizingTextEdit.cpp \ src/widgets/helper/ScrollbarHighlight.cpp \ src/widgets/helper/SearchPopup.cpp \ src/widgets/helper/SettingsDialogTab.cpp \ + src/widgets/helper/SignalLabel.cpp \ + src/widgets/helper/TitlebarButton.cpp \ + src/widgets/Label.cpp \ src/widgets/Notebook.cpp \ src/widgets/Scrollbar.cpp \ src/widgets/settingspages/AboutPage.cpp \ @@ -227,22 +280,28 @@ SOURCES += \ src/widgets/splits/SplitInput.cpp \ src/widgets/splits/SplitOverlay.cpp \ src/widgets/StreamView.cpp \ + src/widgets/TooltipWidget.cpp \ src/widgets/Window.cpp \ HEADERS += \ src/Application.hpp \ src/autogenerated/ResourcesAutogen.hpp \ + src/BaseSettings.hpp \ + src/BaseTheme.hpp \ src/BrowserExtension.hpp \ src/common/Aliases.hpp \ + src/common/Args.hpp \ src/common/Atomic.hpp \ src/common/Channel.hpp \ src/common/ChannelChatters.hpp \ + src/common/ChatterinoSetting.hpp \ src/common/Common.hpp \ src/common/CompletionModel.hpp \ src/common/ConcurrentMap.hpp \ src/common/Credentials.hpp \ src/common/DownloadManager.hpp \ src/common/Env.hpp \ + src/common/FlagsEnum.hpp \ src/common/LinkParser.hpp \ src/common/Modes.hpp \ src/common/NetworkCommon.hpp \ @@ -251,9 +310,11 @@ HEADERS += \ src/common/NetworkRequest.hpp \ src/common/NetworkResult.hpp \ src/common/NullablePtr.hpp \ + src/common/Outcome.hpp \ src/common/ProviderId.hpp \ src/common/SignalVector.hpp \ src/common/SignalVectorModel.hpp \ + src/common/Singleton.hpp \ src/common/UniqueAccess.hpp \ src/common/UsernameSet.hpp \ src/common/Version.hpp \ @@ -282,6 +343,9 @@ HEADERS += \ src/controllers/taggedusers/TaggedUser.hpp \ src/controllers/taggedusers/TaggedUsersController.hpp \ src/controllers/taggedusers/TaggedUsersModel.hpp \ + src/debug/AssertInGuiThread.hpp \ + src/debug/Benchmark.hpp \ + src/debug/Log.hpp \ src/ForwardDecl.hpp \ src/messages/Emote.hpp \ src/messages/Image.hpp \ @@ -327,6 +391,7 @@ HEADERS += \ src/providers/twitch/TwitchAccount.hpp \ src/providers/twitch/TwitchAccountManager.hpp \ src/providers/twitch/TwitchApi.hpp \ + src/providers/twitch/TwitchBadge.hpp \ src/providers/twitch/TwitchBadges.hpp \ src/providers/twitch/TwitchChannel.hpp \ src/providers/twitch/TwitchCommon.hpp \ @@ -339,6 +404,7 @@ HEADERS += \ src/RunGui.hpp \ src/singletons/Badges.hpp \ src/singletons/Emotes.hpp \ + src/singletons/Fonts.hpp \ src/singletons/helper/GifTimer.hpp \ src/singletons/helper/LoggingChannel.hpp \ src/singletons/Logging.hpp \ @@ -351,29 +417,44 @@ HEADERS += \ src/singletons/TooltipPreviewImage.hpp \ src/singletons/Updates.hpp \ src/singletons/WindowManager.hpp \ + src/util/Clamp.hpp \ + src/util/CombinePath.hpp \ src/util/ConcurrentMap.hpp \ src/util/DebugCount.hpp \ + src/util/DistanceBetweenPoints.hpp \ src/util/FormatTime.hpp \ + src/util/FunctionEventFilter.hpp \ + src/util/FuzzyConvert.hpp \ + src/util/Helpers.hpp \ src/util/IncognitoBrowser.hpp \ src/util/InitUpdateButton.hpp \ src/util/IrcHelpers.hpp \ src/util/IsBigEndian.hpp \ src/util/JsonQuery.hpp \ src/util/LayoutCreator.hpp \ + src/util/LayoutHelper.hpp \ src/util/Overloaded.hpp \ + src/util/PostToThread.hpp \ src/util/QObjectRef.hpp \ src/util/QStringHash.hpp \ src/util/rangealgorithm.hpp \ src/util/RapidjsonHelpers.hpp \ + src/util/RapidJsonSerializeQString.hpp \ src/util/RemoveScrollAreaBackground.hpp \ src/util/SampleCheerMessages.hpp \ + src/util/SampleLinks.hpp \ src/util/SharedPtrElementLess.hpp \ + src/util/Shortcut.hpp \ src/util/StandardItemHelper.hpp \ src/util/StreamLink.hpp \ src/util/NuulsUploader.hpp \ + src/util/WindowsHelper.hpp \ src/widgets/AccountSwitchPopup.hpp \ src/widgets/AccountSwitchWidget.hpp \ src/widgets/AttachedWindow.hpp \ + src/widgets/BasePopup.hpp \ + src/widgets/BaseWidget.hpp \ + src/widgets/BaseWindow.hpp \ src/widgets/dialogs/EmotePopup.hpp \ src/widgets/dialogs/IrcConnectionEditor.hpp \ src/widgets/dialogs/LastRunCrashDialog.hpp \ @@ -387,11 +468,13 @@ HEADERS += \ src/widgets/dialogs/UpdateDialog.hpp \ src/widgets/dialogs/UserInfoPopup.hpp \ src/widgets/dialogs/WelcomeDialog.hpp \ + src/widgets/helper/Button.hpp \ src/widgets/helper/ChannelView.hpp \ src/widgets/helper/ComboBoxItemDelegate.hpp \ src/widgets/helper/CommonTexts.hpp \ src/widgets/helper/DebugPopup.hpp \ src/widgets/helper/EditableModelView.hpp \ + src/widgets/helper/EffectLabel.hpp \ src/widgets/helper/Line.hpp \ src/widgets/helper/NotebookButton.hpp \ src/widgets/helper/NotebookTab.hpp \ @@ -399,6 +482,9 @@ HEADERS += \ src/widgets/helper/ScrollbarHighlight.hpp \ src/widgets/helper/SearchPopup.hpp \ src/widgets/helper/SettingsDialogTab.hpp \ + src/widgets/helper/SignalLabel.hpp \ + src/widgets/helper/TitlebarButton.hpp \ + src/widgets/Label.hpp \ src/widgets/Notebook.hpp \ src/widgets/Scrollbar.hpp \ src/widgets/settingspages/AboutPage.hpp \ @@ -419,6 +505,7 @@ HEADERS += \ src/widgets/splits/SplitInput.hpp \ src/widgets/splits/SplitOverlay.hpp \ src/widgets/StreamView.hpp \ + src/widgets/TooltipWidget.hpp \ src/widgets/Window.hpp \ RESOURCES += \ @@ -441,13 +528,13 @@ linux:isEmpty(PREFIX) { } linux { - desktop.files = resources/chatterino.desktop + desktop.files = resources/com.chatterino.chatterino.desktop desktop.path = $$PREFIX/share/applications build_icons.path = . - build_icons.commands = @echo $$PWD && mkdir -p $$PWD/resources/linuxinstall/icons/hicolor/256x256 && cp $$PWD/resources/icon.png $$PWD/resources/linuxinstall/icons/hicolor/256x256/chatterino.png + build_icons.commands = @echo $$PWD && mkdir -p $$PWD/resources/linuxinstall/icons/hicolor/256x256 && cp $$PWD/resources/icon.png $$PWD/resources/linuxinstall/icons/hicolor/256x256/com.chatterino.chatterino.png - icon.files = $$PWD/resources/linuxinstall/icons/hicolor/256x256/chatterino.png + icon.files = $$PWD/resources/linuxinstall/icons/hicolor/256x256/com.chatterino.chatterino.png icon.path = $$PREFIX/share/icons/hicolor/256x256/apps target.path = $$PREFIX/bin diff --git a/conanfile.txt b/conanfile.txt index adde363f..55ac4661 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,12 +1,12 @@ [requires] -OpenSSL/1.0.2o@conan/stable -boost/1.69.0@conan/stable +openssl/1.1.1d +boost/1.71.0 [generators] qmake [options] -OpenSSL:shared=True +openssl:shared=True [imports] bin, *.dll -> ./Chatterino2 @ keep_path=False diff --git a/docs/ENV.md b/docs/ENV.md index 6c83da4a..3fb19d80 100644 --- a/docs/ENV.md +++ b/docs/ENV.md @@ -30,3 +30,16 @@ Notes: - If you want to host the images yourself. You need [Nuuls' filehost software](https://github.com/nuuls/fiehost) - Other image hosting software is currently not supported. +### CHATTERINO2_TWITCH_SERVER_HOST +String value used to change what Twitch chat server host to connect to. +Default value: `irc.chat.twitch.tv` + +### CHATTERINO2_TWITCH_SERVER_PORT +Number value used to change what port to use when connecting to Twitch chat servers. +Currently known valid ports for secure usage: 6697, 443. +Currently known valid ports for non-secure usage (CHATTERINO2_TWITCH_SERVER_SECURE set to false): 6667, 80. +Default value: `443` + +### CHATTERINO2_TWITCH_SERVER_SECURE +Bool value used to tell Chatterino whether to try to connect securely (secure irc) to the Twitch chat server. +Default value: `true` \ No newline at end of file diff --git a/lib/appbase.pri b/lib/appbase.pri deleted file mode 100644 index cbae08c5..00000000 --- a/lib/appbase.pri +++ /dev/null @@ -1,3 +0,0 @@ -# include appbase -include($$PWD/appbase/main.pro) -INCLUDEPATH += $$PWD/appbase diff --git a/lib/appbase/.clang-format b/lib/appbase/.clang-format deleted file mode 100644 index fd28e7c2..00000000 --- a/lib/appbase/.clang-format +++ /dev/null @@ -1,34 +0,0 @@ -Language: Cpp - -AccessModifierOffset: -1 -AccessModifierOffset: -4 -AlignEscapedNewlinesLeft: true -AllowShortFunctionsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLoopsOnASingleLine: false -AlwaysBreakAfterDefinitionReturnType: false -AlwaysBreakBeforeMultilineStrings: false -BasedOnStyle: Google -BraceWrapping: { - AfterNamespace: 'false' - AfterClass: 'true' - BeforeElse: 'true' - AfterControlStatement: 'true' - AfterFunction: 'true' - BeforeCatch: 'true' -} -BreakBeforeBraces: Custom -BreakConstructorInitializersBeforeComma: true -ColumnLimit: 80 -ConstructorInitializerAllOnOneLineOrOnePerLine: false -DerivePointerBinding: false -FixNamespaceComments: true -IndentCaseLabels: true -IndentWidth: 4 -IndentWrappedFunctionNames: true -IndentPPDirectives: AfterHash -NamespaceIndentation: Inner -PointerBindsToType: false -SpacesBeforeTrailingComments: 2 -Standard: Auto -ReflowComments: false diff --git a/lib/appbase/common/Aliases.hpp b/lib/appbase/common/Aliases.hpp deleted file mode 100644 index 2dea876b..00000000 --- a/lib/appbase/common/Aliases.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include -#include - -#define QStringAlias(name) \ - namespace chatterino { \ - struct name { \ - QString string; \ - bool operator==(const name &other) const \ - { \ - return this->string == other.string; \ - } \ - bool operator!=(const name &other) const \ - { \ - return this->string != other.string; \ - } \ - }; \ - } /* namespace chatterino */ \ - namespace std { \ - template <> \ - struct hash { \ - size_t operator()(const chatterino::name &s) const \ - { \ - return qHash(s.string); \ - } \ - }; \ - } /* namespace std */ - -QStringAlias(UserName); -QStringAlias(UserId); -QStringAlias(Url); -QStringAlias(Tooltip); -QStringAlias(EmoteId); -QStringAlias(EmoteName); diff --git a/lib/appbase/main.cpp b/lib/appbase/main.cpp deleted file mode 100644 index 6b95a9c2..00000000 --- a/lib/appbase/main.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include -#include -#include -#include - -#include "ABSettings.hpp" -#include "ABTheme.hpp" -#include "singletons/Fonts.hpp" -#include "widgets/BaseWindow.hpp" - -int main(int argc, char *argv[]) -{ - using namespace AB_NAMESPACE; - - QApplication a(argc, argv); - - auto path = - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); - qDebug() << path; - - QDir(path).mkdir("."); - - new Settings(path); - new Fonts(); - - BaseWindow widget(nullptr, BaseWindow::EnableCustomFrame); - widget.setWindowTitle("asdf"); - widget.show(); - - return a.exec(); -} diff --git a/lib/appbase/main.pro b/lib/appbase/main.pro deleted file mode 100644 index bc464553..00000000 --- a/lib/appbase/main.pro +++ /dev/null @@ -1,100 +0,0 @@ -#------------------------------------------------- -# -# Project created by QtCreator 2018-11-19T19:03:22 -# -#------------------------------------------------- - -!AB_NOT_STANDALONE { - message(appbase standalone) - QT += core gui widgets - TARGET = main - TEMPLATE = app - SOURCES += main.cpp - - # https://bugreports.qt.io/browse/QTBUG-27018 - equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { - TARGET = bin/appbase - } -} - -#DEFINES += QT_DEPRECATED_WARNINGS -#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 - -macx { - # osx (Tested on macOS Mojave and High Sierra) - CONFIG += c++17 -} else { - CONFIG += c++17 - win32-msvc* { - # win32 msvc - QMAKE_CXXFLAGS += /std:c++17 - } else { - # clang/gcc on linux or win32 - QMAKE_CXXFLAGS += -std=c++17 - } -} - -debug { - DEFINES += QT_DEBUG -} - -linux { - LIBS += -lrt - QMAKE_LFLAGS += -lrt -} - -macx { - INCLUDEPATH += /usr/local/include - INCLUDEPATH += /usr/local/opt/openssl/include - LIBS += -L/usr/local/opt/openssl/lib -} - -SOURCES += \ - $$PWD/BaseSettings.cpp \ - $$PWD/BaseTheme.cpp \ - $$PWD/common/ChatterinoSetting.cpp \ - $$PWD/debug/Benchmark.cpp \ - $$PWD/singletons/Fonts.cpp \ - $$PWD/util/FunctionEventFilter.cpp \ - $$PWD/util/FuzzyConvert.cpp \ - $$PWD/util/Helpers.cpp \ - $$PWD/util/WindowsHelper.cpp \ - $$PWD/widgets/BaseWidget.cpp \ - $$PWD/widgets/BaseWindow.cpp \ - $$PWD/widgets/Label.cpp \ - $$PWD/widgets/TooltipWidget.cpp \ - $$PWD/widgets/helper/Button.cpp \ - $$PWD/widgets/helper/EffectLabel.cpp \ - $$PWD/widgets/helper/SignalLabel.cpp \ - $$PWD/widgets/helper/TitlebarButton.cpp \ - -HEADERS += \ - $$PWD/BaseSettings.hpp \ - $$PWD/BaseTheme.hpp \ - $$PWD/common/ChatterinoSetting.hpp \ - $$PWD/common/FlagsEnum.hpp \ - $$PWD/common/Outcome.hpp \ - $$PWD/common/Singleton.hpp \ - $$PWD/debug/AssertInGuiThread.hpp \ - $$PWD/debug/Benchmark.hpp \ - $$PWD/debug/Log.hpp \ - $$PWD/singletons/Fonts.hpp \ - $$PWD/util/Clamp.hpp \ - $$PWD/util/CombinePath.hpp \ - $$PWD/util/DistanceBetweenPoints.hpp \ - $$PWD/util/FunctionEventFilter.hpp \ - $$PWD/util/FuzzyConvert.hpp \ - $$PWD/util/Helpers.hpp \ - $$PWD/util/LayoutHelper.hpp \ - $$PWD/util/PostToThread.hpp \ - $$PWD/util/RapidJsonSerializeQString.hpp \ - $$PWD/util/Shortcut.hpp \ - $$PWD/util/WindowsHelper.hpp \ - $$PWD/widgets/BaseWidget.hpp \ - $$PWD/widgets/BaseWindow.hpp \ - $$PWD/widgets/Label.hpp \ - $$PWD/widgets/TooltipWidget.hpp \ - $$PWD/widgets/helper/Button.hpp \ - $$PWD/widgets/helper/EffectLabel.hpp \ - $$PWD/widgets/helper/SignalLabel.hpp \ - $$PWD/widgets/helper/TitlebarButton.hpp \ diff --git a/lib/boost.pri b/lib/boost.pri index 044de1de..bea78de4 100644 --- a/lib/boost.pri +++ b/lib/boost.pri @@ -1,3 +1,7 @@ +# boost +# Exposed build flags: +# - BOOST_DIRECTORY (C:\local\boost\ by default) (Windows only) + pajlada { BOOST_DIRECTORY = C:\dev\projects\boost_1_66_0\ } diff --git a/lib/fmt.pri b/lib/fmt.pri index 04d34399..8703d2bd 100644 --- a/lib/fmt.pri +++ b/lib/fmt.pri @@ -1,4 +1,18 @@ # fmt -SOURCES += $$PWD/fmt/fmt/format.cpp +# Chatterino2 is tested with FMT 4.0 +# Exposed build flags: +# - FMT_PREFIX ($$PWD by default) +# - FMT_SYSTEM (1 = true) (Linux only, uses pkg-config) -INCLUDEPATH += $$PWD/fmt/ +!defined(FMT_PREFIX) { + FMT_PREFIX = $$PWD +} + +linux:equals(FMT_SYSTEM, "1") { + message("Building with system FMT") + PKGCONFIG += fmt +} else { + SOURCES += $$FMT_PREFIX/fmt/fmt/format.cpp + + INCLUDEPATH += $$PWD/fmt/ +} diff --git a/lib/libcommuni b/lib/libcommuni index a31ffb03..f3e7f979 160000 --- a/lib/libcommuni +++ b/lib/libcommuni @@ -1 +1 @@ -Subproject commit a31ffb037eadac65dba73ad2b2da6dafe31e3bf7 +Subproject commit f3e7f97914d9bf1166d349a83d93a2b4f4743c39 diff --git a/lib/rapidjson.pri b/lib/rapidjson.pri index 5b614e23..131f0401 100644 --- a/lib/rapidjson.pri +++ b/lib/rapidjson.pri @@ -1,2 +1,15 @@ # rapidjson -INCLUDEPATH += $$PWD/rapidjson/include/ +# Chatterino2 is tested with RapidJSON v1.1.0 +# - RAPIDJSON_PREFIX ($$PWD by default) +# - RAPIDJSON_SYSTEM (1 = true) (Linux only, uses pkg-config) + +!defined(RAPIDJSON_PREFIX) { + RAPIDJSON_PREFIX = $$PWD +} + +linux:equals(RAPIDJSON_SYSTEM, "1") { + message("Building with system RapidJSON") + PKGCONFIG += RapidJSON +} else { + INCLUDEPATH += $$RAPIDJSON_PREFIX/rapidjson/include/ +} diff --git a/lib/websocketpp.pri b/lib/websocketpp.pri index ae3ea1e4..2ae1289b 100644 --- a/lib/websocketpp.pri +++ b/lib/websocketpp.pri @@ -1 +1,20 @@ -INCLUDEPATH += $$PWD/../lib/websocketpp +# websocketpp +# Chatterino2 is tested with websocketpp 0.8.1 +# Exposed build flags: +# - WEBSOCKETPP_PREFIX ($$PWD by default) +# - WEBSOCKETPP_SYSTEM (1 = true) (unix only) + +!defined(WEBSOCKETPP_PREFIX) { + WEBSOCKETPP_PREFIX = $$PWD +} + +unix { + equals(WEBSOCKETPP_SYSTEM, "1") { + message("Building with system websocketpp") + } else { + message("Building with websocketpp submodule (Prefix: $$WEBSOCKETPP_PREFIX)") + INCLUDEPATH += $$WEBSOCKETPP_PREFIX/websocketpp/ + } +} else { + INCLUDEPATH += $$WEBSOCKETPP_PREFIX/websocketpp +} diff --git a/resources/com.chatterino.chatterino.appdata.xml b/resources/com.chatterino.chatterino.appdata.xml new file mode 100644 index 00000000..7913fbe3 --- /dev/null +++ b/resources/com.chatterino.chatterino.appdata.xml @@ -0,0 +1,35 @@ + + + + com.chatterino.chatterino.desktop + CC0-1.0 + MIT + + intense + + Chatterino + + Chat client for twitch.tv + + +

+ Chatterino 2 is the second installment of the Twitch chat client series + "Chatterino". +

+
+ + + https://chatterino.com/img/screenshot-3.png + + + + chat + twitch + + https://chatterino.com/ + https://github.com/Chatterino/chatterino2/issues + https://streamelements.com/fourtf/tip + + chatterino + +
diff --git a/resources/chatterino.desktop b/resources/com.chatterino.chatterino.desktop similarity index 100% rename from resources/chatterino.desktop rename to resources/com.chatterino.chatterino.desktop diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index 04deaf23..0ba21f2f 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -28,7 +28,7 @@ buttons/unmod.png buttons/update.png buttons/updateError.png - chatterino.desktop + com.chatterino.chatterino.desktop chatterino.icns contributors.txt emoji.json diff --git a/src/.clang-format b/src/.clang-format index fd28e7c2..0ef59b1f 100644 --- a/src/.clang-format +++ b/src/.clang-format @@ -1,6 +1,5 @@ Language: Cpp -AccessModifierOffset: -1 AccessModifierOffset: -4 AlignEscapedNewlinesLeft: true AllowShortFunctionsOnASingleLine: false @@ -10,12 +9,12 @@ AlwaysBreakAfterDefinitionReturnType: false AlwaysBreakBeforeMultilineStrings: false BasedOnStyle: Google BraceWrapping: { - AfterNamespace: 'false' AfterClass: 'true' - BeforeElse: 'true' AfterControlStatement: 'true' AfterFunction: 'true' + AfterNamespace: 'false' BeforeCatch: 'true' + BeforeElse: 'true' } BreakBeforeBraces: Custom BreakConstructorInitializersBeforeComma: true @@ -27,6 +26,7 @@ IndentCaseLabels: true IndentWidth: 4 IndentWrappedFunctionNames: true IndentPPDirectives: AfterHash +IncludeBlocks: Preserve NamespaceIndentation: Inner PointerBindsToType: false SpacesBeforeTrailingComments: 2 diff --git a/src/Application.cpp b/src/Application.cpp index c0749037..776fef60 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -1,5 +1,8 @@ #include "Application.hpp" +#include + +#include "common/Args.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/commands/CommandController.hpp" #include "controllers/highlights/HighlightController.hpp" @@ -29,9 +32,9 @@ #include "singletons/WindowManager.hpp" #include "util/IsBigEndian.hpp" #include "util/PostToThread.hpp" +#include "widgets/Notebook.hpp" #include "widgets/Window.hpp" - -#include +#include "widgets/splits/Split.hpp" namespace chatterino { @@ -44,9 +47,7 @@ Application *Application::instance = nullptr; // to each other Application::Application(Settings &_settings, Paths &_paths) - : resources(&this->emplace()) - - , themes(&this->emplace()) + : themes(&this->emplace()) , fonts(&this->emplace()) , emotes(&this->emplace()) , windows(&this->emplace()) @@ -79,13 +80,38 @@ void Application::initialize(Settings &settings, Paths &paths) assert(isAppInitialized == false); isAppInitialized = true; - Irc::getInstance().load(); + if (getSettings()->enableExperimentalIrc) + { + Irc::instance().load(); + } for (auto &singleton : this->singletons_) { singleton->initialize(settings, paths); } + // add crash message + if (getArgs().crashRecovery) + { + if (auto selected = + this->windows->getMainWindow().getNotebook().getSelectedPage()) + { + if (auto container = dynamic_cast(selected)) + { + for (auto &&split : container->getSplits()) + { + if (auto channel = split->getChannel(); !channel->isEmpty()) + { + channel->addMessage(makeSystemMessage( + "Chatterino unexpectedly crashed and restarted. " + "You can disable automatic restarts in the " + "settings.")); + } + } + } + } + } + this->windows->updateWordTypeMask(); this->initNm(paths); @@ -104,7 +130,7 @@ int Application::run(QApplication &qtApp) this->windows->getMainWindow().show(); getSettings()->betaUpdates.connect( - [] { Updates::getInstance().checkForUpdates(); }, false); + [] { Updates::instance().checkForUpdates(); }, false); return qtApp.exec(); } diff --git a/src/Application.hpp b/src/Application.hpp index 88cea1ab..fac8ec0f 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -1,11 +1,11 @@ #pragma once -#include "common/Singleton.hpp" -#include "singletons/NativeMessaging.hpp" - #include #include +#include "common/Singleton.hpp" +#include "singletons/NativeMessaging.hpp" + namespace chatterino { class TwitchIrcServer; @@ -28,7 +28,6 @@ class AccountManager; class Emotes; class Settings; class Fonts; -class Resources2; class Toasts; class ChatterinoBadges; @@ -51,8 +50,6 @@ public: friend void test(); - Resources2 *const resources; - Theme *const themes{}; Fonts *const fonts{}; Emotes *const emotes{}; diff --git a/lib/appbase/BaseSettings.cpp b/src/BaseSettings.cpp similarity index 98% rename from lib/appbase/BaseSettings.cpp rename to src/BaseSettings.cpp index 01897555..8d29fccb 100644 --- a/lib/appbase/BaseSettings.cpp +++ b/src/BaseSettings.cpp @@ -4,7 +4,7 @@ #include "util/Clamp.hpp" -namespace AB_NAMESPACE { +namespace chatterino { std::vector> _settings; @@ -125,4 +125,4 @@ AB_SETTINGS_CLASS *getABSettings() return AB_SETTINGS_CLASS::instance; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/BaseSettings.hpp b/src/BaseSettings.hpp similarity index 95% rename from lib/appbase/BaseSettings.hpp rename to src/BaseSettings.hpp index 58b1f846..0e1ff58a 100644 --- a/lib/appbase/BaseSettings.hpp +++ b/src/BaseSettings.hpp @@ -14,7 +14,7 @@ # define AB_SETTINGS_CLASS Settings #endif -namespace AB_NAMESPACE { +namespace chatterino { class Settings; @@ -44,7 +44,7 @@ private: Settings *getSettings(); AB_SETTINGS_CLASS *getABSettings(); -} // namespace AB_NAMESPACE +} // namespace chatterino #ifdef CHATTERINO # include "singletons/Settings.hpp" diff --git a/lib/appbase/BaseTheme.cpp b/src/BaseTheme.cpp similarity index 96% rename from lib/appbase/BaseTheme.cpp rename to src/BaseTheme.cpp index ab4e314d..99ddddb2 100644 --- a/lib/appbase/BaseTheme.cpp +++ b/src/BaseTheme.cpp @@ -1,6 +1,6 @@ #include "BaseTheme.hpp" -namespace AB_NAMESPACE { +namespace chatterino { namespace { double getMultiplierByTheme(const QString &themeName) { @@ -63,9 +63,9 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier) /// WINDOW { #ifdef Q_OS_LINUX - this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); + this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); #else - this->window.background = lightWin ? "#fff" : "#111"; + this->window.background = lightWin ? "#fff" : "#111"; #endif QColor fg = this->window.text = lightWin ? "#000" : "#eee"; @@ -149,10 +149,7 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier) // QColor("#777"), QColor("#666")}}; this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color(); - } // namespace AB_NAMESPACE - - // Split - bool flat = isLight_; + } // Message this->messages.textColors.link = @@ -222,4 +219,4 @@ Theme *getTheme() } #endif -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/BaseTheme.hpp b/src/BaseTheme.hpp similarity index 97% rename from lib/appbase/BaseTheme.hpp rename to src/BaseTheme.hpp index 21c6c46c..d9980813 100644 --- a/lib/appbase/BaseTheme.hpp +++ b/src/BaseTheme.hpp @@ -11,7 +11,7 @@ # define AB_THEME_CLASS Theme #endif -namespace AB_NAMESPACE { +namespace chatterino { class Theme; @@ -109,7 +109,7 @@ private: // Otherwise implemented in BaseThemecpp Theme *getTheme(); -} // namespace AB_NAMESPACE +} // namespace chatterino #ifdef CHATTERINO # include "singletons/Theme.hpp" diff --git a/src/RunGui.cpp b/src/RunGui.cpp index 5f86a6a4..3088b6da 100644 --- a/src/RunGui.cpp +++ b/src/RunGui.cpp @@ -4,10 +4,16 @@ #include #include #include +#include +#include +#include #include "Application.hpp" +#include "common/Modes.hpp" #include "common/NetworkManager.hpp" #include "singletons/Paths.hpp" +#include "singletons/Resources.hpp" +#include "singletons/Settings.hpp" #include "singletons/Updates.hpp" #include "util/CombinePath.hpp" #include "widgets/dialogs/LastRunCrashDialog.hpp" @@ -20,12 +26,6 @@ # include #endif -// void initQt(); -// void installCustomPalette(); -// void showLastCrashDialog(); -// void createRunningFile(const QString &path); -// void removeRunningFile(const QString &path); - namespace chatterino { namespace { void installCustomPalette() @@ -108,11 +108,68 @@ namespace { { QFile::remove(path); } + + std::chrono::steady_clock::time_point signalsInitTime; + bool restartOnSignal = false; + + [[noreturn]] void handleSignal(int signum) + { + using namespace std::chrono_literals; + + if (restartOnSignal && + std::chrono::steady_clock::now() - signalsInitTime > 30s) + { + QProcess proc; + proc.setProgram(QApplication::applicationFilePath()); + proc.setArguments({"--crash-recovery"}); + proc.startDetached(); + } + + _exit(signum); + } + + // We want to restart chatterino when it crashes and the setting is set to + // true. + void initSignalHandler() + { +#ifndef C_DEBUG + signalsInitTime = std::chrono::steady_clock::now(); + + signal(SIGSEGV, handleSignal); +#endif + } + + // We delete cache files that haven't been modified in 14 days. This strategy may be + // improved in the future. + void clearCache(const QDir &dir) + { + qDebug() << "[Cache] cleared cache"; + + QStringList toBeRemoved; + + for (auto &&info : dir.entryInfoList(QDir::Files)) + { + if (info.lastModified().addDays(14) < QDateTime::currentDateTime()) + { + toBeRemoved << info.absoluteFilePath(); + } + } + + for (auto &&path : toBeRemoved) + { + qDebug() << path << QFile(path).remove(); + } + } } // namespace void runGui(QApplication &a, Paths &paths, Settings &settings) { initQt(); + initResources(); + initSignalHandler(); + + settings.restartOnCrash.connect( + [](const bool &value) { restartOnSignal = value; }); auto thread = std::thread([dir = paths.miscDirectory] { { @@ -131,8 +188,13 @@ void runGui(QApplication &a, Paths &paths, Settings &settings) } }); + // Clear the cache 1 minute after start. + QTimer::singleShot(60 * 1000, [cachePath = paths.cacheDirectory()] { + QtConcurrent::run([cachePath]() { clearCache(cachePath); }); + }); + chatterino::NetworkManager::init(); - chatterino::Updates::getInstance().checkForUpdates(); + chatterino::Updates::instance().checkForUpdates(); #ifdef C_USE_BREAKPAD QBreakpadInstance.setDumpPath(getPaths()->settingsFolderPath + "/Crashes"); diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp index 9629a8f0..e7aab5d5 100644 --- a/src/autogenerated/ResourcesAutogen.cpp +++ b/src/autogenerated/ResourcesAutogen.cpp @@ -50,4 +50,4 @@ Resources2::Resources2() this->twitch.vip = QPixmap(":/twitch/vip.png"); } -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp index 51a9a573..cb342363 100644 --- a/src/autogenerated/ResourcesAutogen.hpp +++ b/src/autogenerated/ResourcesAutogen.hpp @@ -1,4 +1,5 @@ #include + #include "common/Singleton.hpp" namespace chatterino { @@ -64,4 +65,4 @@ public: } twitch; }; -} // namespace chatterino \ No newline at end of file +} // namespace chatterino diff --git a/src/common/Args.cpp b/src/common/Args.cpp new file mode 100644 index 00000000..0eaf5b35 --- /dev/null +++ b/src/common/Args.cpp @@ -0,0 +1,34 @@ +#include "Args.hpp" + +namespace chatterino { + +Args::Args(const QStringList &args) +{ + for (auto &&arg : args) + { + if (arg == "--crash-recovery") + { + this->crashRecovery = true; + } + else if (arg == "--version") + { + this->printVersion = true; + } + } +} + +static Args *instance = nullptr; + +void initArgs(const QStringList &args) +{ + instance = new Args(args); +} + +const Args &getArgs() +{ + assert(instance); + + return *instance; +} + +} // namespace chatterino diff --git a/src/common/Args.hpp b/src/common/Args.hpp new file mode 100644 index 00000000..cde9538b --- /dev/null +++ b/src/common/Args.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace chatterino { + +/// Command line arguments passed to Chatterino. +class Args +{ +public: + Args(const QStringList &args); + + bool printVersion{}; + bool crashRecovery{}; +}; + +void initArgs(const QStringList &args); +const Args &getArgs(); + +} // namespace chatterino diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 1c13b6db..e1c5ba0d 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -178,7 +178,7 @@ void Channel::addOrReplaceTimeout(MessagePtr message) } // XXX: Might need the following line - // WindowManager::getInstance().repaintVisibleChatWidgets(this); + // WindowManager::instance().repaintVisibleChatWidgets(this); } void Channel::disableAllMessages() diff --git a/lib/appbase/common/ChatterinoSetting.cpp b/src/common/ChatterinoSetting.cpp similarity index 78% rename from lib/appbase/common/ChatterinoSetting.cpp rename to src/common/ChatterinoSetting.cpp index 0f249c28..100babaa 100644 --- a/lib/appbase/common/ChatterinoSetting.cpp +++ b/src/common/ChatterinoSetting.cpp @@ -2,11 +2,11 @@ #include "BaseSettings.hpp" -namespace AB_NAMESPACE { +namespace chatterino { void _registerSetting(std::weak_ptr setting) { _actuallyRegisterSetting(setting); } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/common/ChatterinoSetting.hpp b/src/common/ChatterinoSetting.hpp similarity index 97% rename from lib/appbase/common/ChatterinoSetting.hpp rename to src/common/ChatterinoSetting.hpp index d7764841..2e48956b 100644 --- a/lib/appbase/common/ChatterinoSetting.hpp +++ b/src/common/ChatterinoSetting.hpp @@ -3,7 +3,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { void _registerSetting(std::weak_ptr setting); @@ -85,4 +85,4 @@ public: } }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/common/Common.hpp b/src/common/Common.hpp index fe5acc56..43b95407 100644 --- a/src/common/Common.hpp +++ b/src/common/Common.hpp @@ -1,16 +1,15 @@ #pragma once -#include "common/Aliases.hpp" -#include "common/Outcome.hpp" -#include "common/ProviderId.hpp" - #include #include #include #include - #include +#include "common/Aliases.hpp" +#include "common/Outcome.hpp" +#include "common/ProviderId.hpp" + namespace chatterino { enum class HighlightState { @@ -46,4 +45,14 @@ enum class CopyMode { OnlyTextAndEmotes, }; +struct DeleteLater { + void operator()(QObject *obj) + { + obj->deleteLater(); + } +}; + +template +using QObjectPtr = std::unique_ptr; + } // namespace chatterino diff --git a/src/common/Credentials.cpp b/src/common/Credentials.cpp index 16efc448..87abecb9 100644 --- a/src/common/Credentials.cpp +++ b/src/common/Credentials.cpp @@ -143,7 +143,7 @@ namespace { } } // namespace -Credentials &Credentials::getInstance() +Credentials &Credentials::instance() { static Credentials creds; return creds; @@ -166,11 +166,12 @@ void Credentials::get(const QString &provider, const QString &name_, auto job = new QKeychain::ReadPasswordJob("chatterino"); job->setAutoDelete(true); job->setKey(name); - QObject::connect(job, &QKeychain::Job::finished, receiver, - [job, onLoaded = std::move(onLoaded)](auto) mutable { - onLoaded(job->textData()); - }, - Qt::DirectConnection); + QObject::connect( + job, &QKeychain::Job::finished, receiver, + [job, onLoaded = std::move(onLoaded)](auto) mutable { + onLoaded(job->textData()); + }, + Qt::DirectConnection); job->start(); } else @@ -199,7 +200,9 @@ void Credentials::set(const QString &provider, const QString &name_, { auto &instance = insecureInstance(); - instance.object()[name] = credential; + auto obj = instance.object(); + obj[name] = credential; + instance.setObject(obj); queueInsecureSave(); } diff --git a/src/common/Credentials.hpp b/src/common/Credentials.hpp index b71ed6cd..5cf9da92 100644 --- a/src/common/Credentials.hpp +++ b/src/common/Credentials.hpp @@ -8,7 +8,7 @@ namespace chatterino { class Credentials { public: - static Credentials &getInstance(); + static Credentials &instance(); void get(const QString &provider, const QString &name, QObject *receiver, std::function &&onLoaded); diff --git a/src/common/DownloadManager.cpp b/src/common/DownloadManager.cpp index 5adc8ef3..a579d37a 100644 --- a/src/common/DownloadManager.cpp +++ b/src/common/DownloadManager.cpp @@ -48,13 +48,11 @@ void DownloadManager::onFinished(QNetworkReply *reply) { switch (reply->error()) { - case QNetworkReply::NoError: - { + case QNetworkReply::NoError: { qDebug("file is downloaded successfully."); } break; - default: - { + default: { qDebug() << reply->errorString().toLatin1(); }; } diff --git a/src/common/Env.cpp b/src/common/Env.cpp index b30cc652..b2712184 100644 --- a/src/common/Env.cpp +++ b/src/common/Env.cpp @@ -1,5 +1,7 @@ #include "common/Env.hpp" +#include + namespace chatterino { namespace { @@ -15,6 +17,33 @@ namespace { return defaultValue; } + uint16_t readPortEnv(const char *envName, uint16_t defaultValue) + { + auto envString = std::getenv(envName); + if (envString != nullptr) + { + bool ok; + auto val = QString(envString).toUShort(&ok); + if (ok) + { + return val; + } + } + + return defaultValue; + } + + uint16_t readBoolEnv(const char *envName, bool defaultValue) + { + auto envString = std::getenv(envName); + if (envString != nullptr) + { + return QVariant(QString(envString)).toBool(); + } + + return defaultValue; + } + } // namespace Env::Env() @@ -30,6 +59,10 @@ Env::Env() "https://braize.pajlada.com/chatterino/twitchemotes/set/%1/")) , imageUploaderUrl(readStringEnv("CHATTERINO2_IMAGE_PASTE_SITE_URL", "https://i.nuuls.com/upload")) + , twitchServerHost( + readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv")) + , twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 6697)) + , twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true)) { } diff --git a/src/common/Env.hpp b/src/common/Env.hpp index 07beef85..29c17b6a 100644 --- a/src/common/Env.hpp +++ b/src/common/Env.hpp @@ -15,6 +15,9 @@ public: const QString linkResolverUrl; const QString twitchEmoteSetResolverUrl; const QString imageUploaderUrl; + const QString twitchServerHost; + const uint16_t twitchServerPort; + const bool twitchServerSecure; }; } // namespace chatterino diff --git a/lib/appbase/common/FlagsEnum.hpp b/src/common/FlagsEnum.hpp similarity index 100% rename from lib/appbase/common/FlagsEnum.hpp rename to src/common/FlagsEnum.hpp diff --git a/src/common/LinkParser.cpp b/src/common/LinkParser.cpp index a80c7564..18c3a29a 100644 --- a/src/common/LinkParser.cpp +++ b/src/common/LinkParser.cpp @@ -1,67 +1,201 @@ #include "common/LinkParser.hpp" #include +#include #include #include +#include #include -// ip 0.0.0.0 - 224.0.0.0 -#define IP \ - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" \ - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" \ - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" -#define PORT "(?::\\d{2,5})" -#define WEB_CHAR1 "[_a-z\\x{00a1}-\\x{ffff}0-9]" -#define WEB_CHAR2 "[a-z\\x{00a1}-\\x{ffff}0-9]" - -#define SPOTIFY_1 "(?:artist|album|track|user:[^:]+:playlist):[a-zA-Z0-9]+" -#define SPOTIFY_2 "user:[^:]+" -#define SPOTIFY_3 "search:(?:[-\\w$\\.+!*'(),]+|%[a-fA-F0-9]{2})+" -#define SPOTIFY_PARAMS "(?:" SPOTIFY_1 "|" SPOTIFY_2 "|" SPOTIFY_3 ")" -#define SPOTIFY_LINK "(?x-mi:(spotify:" SPOTIFY_PARAMS "))" - -#define WEB_PROTOCOL "(?:(?:https?|ftps?)://)?" -#define WEB_USER "(?:\\S+(?::\\S*)?@)?" -#define WEB_HOST "(?:(?:" WEB_CHAR1 "-*)*" WEB_CHAR2 "+)" -#define WEB_DOMAIN "(?:\\.(?:" WEB_CHAR2 "-*)*" WEB_CHAR2 "+)*" -#define WEB_TLD "(?:" + tldData + ")" -#define WEB_RESOURCE_PATH "(?:[/?#]\\S*)" -#define WEB_LINK \ - WEB_PROTOCOL WEB_USER "(?:" IP "|" WEB_HOST WEB_DOMAIN "\\." WEB_TLD PORT \ - "?" WEB_RESOURCE_PATH "?)" - -#define LINK "^(?:" SPOTIFY_LINK "|" WEB_LINK ")$" - namespace chatterino { +namespace { + QSet &tlds() + { + static QSet tlds = [] { + QFile file(":/tlds.txt"); + file.open(QFile::ReadOnly); + QTextStream stream(&file); + stream.setCodec("UTF-8"); + int safetyMax = 20000; + + QSet set; + + while (!stream.atEnd()) + { + auto line = stream.readLine(); + set.insert(line); + + if (safetyMax-- == 0) + break; + } + + return set; + }(); + return tlds; + } + + bool isValidHostname(QStringRef &host) + { + int index = host.lastIndexOf('.'); + + return index != -1 && + tlds().contains(host.mid(index + 1).toString().toLower()); + } + + bool isValidIpv4(QStringRef &host) + { + static auto exp = QRegularExpression("^\\d{1,3}(?:\\.\\d{1,3}){3}$"); + + return exp.match(host).hasMatch(); + } + +#ifdef C_MATCH_IPV6_LINK + bool isValidIpv6(QStringRef &host) + { + static auto exp = QRegularExpression("^\\[[a-fA-F0-9:%]+\\]$"); + + return exp.match(host).hasMatch(); + } +#endif +} // namespace LinkParser::LinkParser(const QString &unparsedString) { - static QRegularExpression linkRegex = [] { - static QRegularExpression newLineRegex("\r?\n"); - QFile file(":/tlds.txt"); - file.open(QFile::ReadOnly); - QTextStream tlds(&file); - tlds.setCodec("UTF-8"); + this->match_ = unparsedString; - // tldData gets injected into the LINK macro - auto tldData = tlds.readAll().replace(newLineRegex, "|"); - (void)tldData; + // This is not implemented with a regex to increase performance. + // We keep removing parts of the url until there's either nothing left or we fail. + QStringRef l(&unparsedString); - return QRegularExpression(LINK, - QRegularExpression::CaseInsensitiveOption); - }(); + bool hasHttp = false; - this->match_ = linkRegex.match(unparsedString); + // Protocol `https?://` + if (l.startsWith("https://", Qt::CaseInsensitive)) + { + hasHttp = true; + l = l.mid(8); + } + else if (l.startsWith("http://", Qt::CaseInsensitive)) + { + hasHttp = true; + l = l.mid(7); + } + + // Http basic auth `user:password`. + // Not supported for security reasons (misleading links) + + // Host `a.b.c.com` + QStringRef host = l; + bool lastWasDot = true; + bool inIpv6 = false; + + for (int i = 0; i < l.size(); i++) + { + if (l[i] == '.') + { + if (lastWasDot == true) // no double dots .. + goto error; + lastWasDot = true; + } + else + { + lastWasDot = false; + } + + if (l[i] == ':' && !inIpv6) + { + host = l.mid(0, i); + l = l.mid(i + 1); + goto parsePort; + } + else if (l[i] == '/') + { + host = l.mid(0, i); + l = l.mid(i + 1); + goto parsePath; + } + else if (l[i] == '?') + { + host = l.mid(0, i); + l = l.mid(i + 1); + goto parseQuery; + } + else if (l[i] == '#') + { + host = l.mid(0, i); + l = l.mid(i + 1); + goto parseAnchor; + } + + // ipv6 + if (l[i] == '[') + { + if (i == 0) + inIpv6 = true; + else + goto error; + } + else if (l[i] == ']') + { + inIpv6 = false; + } + } + + if (lastWasDot) + goto error; + else + goto done; + +parsePort: + // Port `:12345` + for (int i = 0; i < std::min(5, l.size()); i++) + { + if (l[i] == '/') + goto parsePath; + else if (l[i] == '?') + goto parseQuery; + else if (l[i] == '#') + goto parseAnchor; + + if (!l[i].isDigit()) + goto error; + } + + goto done; + +parsePath: +parseQuery: +parseAnchor: + // we accept everything in the path/query/anchor + +done: + // check host + this->hasMatch_ = isValidHostname(host) || isValidIpv4(host) +#ifdef C_MATCH_IPV6_LINK + + || (hasHttp && isValidIpv6(host)) +#endif + ; + + if (this->hasMatch_) + { + this->match_ = unparsedString; + } + + return; + +error: + hasMatch_ = false; } bool LinkParser::hasMatch() const { - return this->match_.hasMatch(); + return this->hasMatch_; } QString LinkParser::getCaptured() const { - return this->match_.captured(); + return this->match_; } } // namespace chatterino diff --git a/src/common/LinkParser.hpp b/src/common/LinkParser.hpp index 2d56cbde..907d22a8 100644 --- a/src/common/LinkParser.hpp +++ b/src/common/LinkParser.hpp @@ -14,7 +14,8 @@ public: QString getCaptured() const; private: - QRegularExpressionMatch match_; + bool hasMatch_{false}; + QString match_; }; } // namespace chatterino diff --git a/src/common/Modes.cpp b/src/common/Modes.cpp index 23c385ec..ead79dea 100644 --- a/src/common/Modes.cpp +++ b/src/common/Modes.cpp @@ -27,7 +27,7 @@ Modes::Modes() } } -const Modes &Modes::getInstance() +const Modes &Modes::instance() { static Modes instance; return instance; diff --git a/src/common/Modes.hpp b/src/common/Modes.hpp index 6bf5cc25..87bef41a 100644 --- a/src/common/Modes.hpp +++ b/src/common/Modes.hpp @@ -7,7 +7,7 @@ class Modes public: Modes(); - static const Modes &getInstance(); + static const Modes &instance(); bool isNightly{}; bool isPortable{}; diff --git a/lib/appbase/common/Outcome.hpp b/src/common/Outcome.hpp similarity index 100% rename from lib/appbase/common/Outcome.hpp rename to src/common/Outcome.hpp diff --git a/lib/appbase/common/Singleton.hpp b/src/common/Singleton.hpp similarity index 85% rename from lib/appbase/common/Singleton.hpp rename to src/common/Singleton.hpp index 5bf3857a..401716d7 100644 --- a/lib/appbase/common/Singleton.hpp +++ b/src/common/Singleton.hpp @@ -2,7 +2,7 @@ #include -namespace AB_NAMESPACE { +namespace chatterino { class Settings; class Paths; @@ -23,4 +23,4 @@ public: } }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/common/Version.cpp b/src/common/Version.cpp index e6a47ef3..4e4707d2 100644 --- a/src/common/Version.cpp +++ b/src/common/Version.cpp @@ -16,50 +16,46 @@ Version::Version() this->commitHash_ = QString(FROM_EXTERNAL_DEFINE(CHATTERINO_GIT_HASH)).remove('"'); - // Date of build + // Date of build, this is depended on the format not changing #ifdef CHATTERINO_NIGHTLY_VERSION_STRING this->dateOfBuild_ = QString(FROM_EXTERNAL_DEFINE(CHATTERINO_NIGHTLY_VERSION_STRING)) - .remove('"'); + .remove('"') + .split(' ')[0]; #endif // "Full" version string, as displayed in window title this->fullVersion_ = "Chatterino "; - if (Modes::getInstance().isNightly) + if (Modes::instance().isNightly) { this->fullVersion_ += "Nightly "; } this->fullVersion_ += this->version_; - - if (Modes::getInstance().isNightly) - { - this->fullVersion_ += this->dateOfBuild_; - } } -const Version &Version::getInstance() +const Version &Version::instance() { static Version instance; return instance; } -const QString &Version::getVersion() const +const QString &Version::version() const { return this->version_; } -const QString &Version::getFullVersion() const +const QString &Version::fullVersion() const { return this->fullVersion_; } -const QString &Version::getCommitHash() const +const QString &Version::commitHash() const { return this->commitHash_; } -const QString &Version::getDateOfBuild() const +const QString &Version::dateOfBuild() const { return this->dateOfBuild_; } diff --git a/src/common/Version.hpp b/src/common/Version.hpp index dd3baa9f..9d00ddc0 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -1,8 +1,9 @@ #pragma once +#include #include -#define CHATTERINO_VERSION "2.1.4" +#define CHATTERINO_VERSION "2.1.7" #if defined(Q_OS_WIN) # define CHATTERINO_OS "win" @@ -19,12 +20,12 @@ namespace chatterino { class Version { public: - static const Version &getInstance(); + static const Version &instance(); - const QString &getVersion() const; - const QString &getCommitHash() const; - const QString &getDateOfBuild() const; - const QString &getFullVersion() const; + const QString &version() const; + const QString &commitHash() const; + const QString &dateOfBuild() const; + const QString &fullVersion() const; private: Version(); @@ -35,4 +36,4 @@ private: QString fullVersion_; }; -}; +}; // namespace chatterino diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp index 1af0cc33..cdaa5328 100644 --- a/src/controllers/accounts/AccountController.cpp +++ b/src/controllers/accounts/AccountController.cpp @@ -27,8 +27,7 @@ AccountController::AccountController() this->accounts_.itemRemoved.connect([this](const auto &args) { switch (args.item->getProviderId()) { - case ProviderId::Twitch: - { + case ProviderId::Twitch: { if (args.caller != this) { auto accs = this->twitch.accounts.cloneVector(); diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index 7f0cc599..85950dd0 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -70,8 +70,7 @@ void HighlightModel::customRowSetData(const std::vector &row, { switch (column) { - case 0: - { + case 0: { if (role == Qt::CheckStateRole) { if (rowIndex == 0) @@ -86,8 +85,7 @@ void HighlightModel::customRowSetData(const std::vector &row, } } break; - case 1: - { + case 1: { if (role == Qt::CheckStateRole) { if (rowIndex == 0) @@ -103,8 +101,7 @@ void HighlightModel::customRowSetData(const std::vector &row, } } break; - case 2: - { + case 2: { if (role == Qt::CheckStateRole) { if (rowIndex == 0) @@ -120,8 +117,7 @@ void HighlightModel::customRowSetData(const std::vector &row, } } break; - case 3: - { + case 3: { // empty element } break; diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index c206e364..e1877bc4 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -119,8 +119,8 @@ struct Deserialize { chatterino::rj::getSafe(value, "regex", _isRegex); chatterino::rj::getSafe(value, "case", _caseSensitive); - return chatterino::HighlightPhrase(_pattern, _alert, _sound, - _isRegex, _caseSensitive); + return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex, + _caseSensitive); } }; diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp index 1bba03d2..dce01904 100644 --- a/src/controllers/moderationactions/ModerationAction.cpp +++ b/src/controllers/moderationactions/ModerationAction.cpp @@ -65,17 +65,17 @@ ModerationAction::ModerationAction(const QString &action) // line1 = this->line1_; // line2 = this->line2_; // } else { - // this->_moderationActions.emplace_back(app->resources->buttonTimeout, + // this->_moderationActions.emplace_back(getResources().buttonTimeout, // str); // } } else if (action.startsWith("/ban ")) { - this->image_ = Image::fromPixmap(getApp()->resources->buttons.ban); + this->image_ = Image::fromPixmap(getResources().buttons.ban); } else if (action.startsWith("/delete ")) { - this->image_ = Image::fromPixmap(getApp()->resources->buttons.trashCan); + this->image_ = Image::fromPixmap(getResources().buttons.trashCan); } else { diff --git a/lib/appbase/debug/AssertInGuiThread.hpp b/src/debug/AssertInGuiThread.hpp similarity index 83% rename from lib/appbase/debug/AssertInGuiThread.hpp rename to src/debug/AssertInGuiThread.hpp index b43f2695..6dc14496 100644 --- a/lib/appbase/debug/AssertInGuiThread.hpp +++ b/src/debug/AssertInGuiThread.hpp @@ -4,7 +4,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { static bool isGuiThread() { @@ -18,4 +18,4 @@ static void assertInGuiThread() #endif } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/debug/Benchmark.cpp b/src/debug/Benchmark.cpp similarity index 86% rename from lib/appbase/debug/Benchmark.cpp rename to src/debug/Benchmark.cpp index afa80cf2..6541545c 100644 --- a/lib/appbase/debug/Benchmark.cpp +++ b/src/debug/Benchmark.cpp @@ -1,6 +1,6 @@ #include "Benchmark.hpp" -namespace AB_NAMESPACE { +namespace chatterino { BenchmarkGuard::BenchmarkGuard(const QString &_name) : name_(_name) @@ -18,4 +18,4 @@ qreal BenchmarkGuard::getElapsedMs() return qreal(timer_.nsecsElapsed()) / 1000000.0; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/debug/Benchmark.hpp b/src/debug/Benchmark.hpp similarity index 84% rename from lib/appbase/debug/Benchmark.hpp rename to src/debug/Benchmark.hpp index c477aa34..065048e8 100644 --- a/lib/appbase/debug/Benchmark.hpp +++ b/src/debug/Benchmark.hpp @@ -5,7 +5,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { class BenchmarkGuard : boost::noncopyable { @@ -19,4 +19,4 @@ private: QString name_; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/debug/Log.hpp b/src/debug/Log.hpp similarity index 92% rename from lib/appbase/debug/Log.hpp rename to src/debug/Log.hpp index af8b136e..31536781 100644 --- a/lib/appbase/debug/Log.hpp +++ b/src/debug/Log.hpp @@ -5,7 +5,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { template inline void log(const std::string &formatString, Args &&... args) @@ -26,4 +26,4 @@ inline void log(const QString &formatString, Args &&... args) log(formatString.toStdString(), std::forward(args)...); } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/main.cpp b/src/main.cpp index 6cce245e..46346589 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,18 @@ +#include +#include +#include +#include +#include + #include "BrowserExtension.hpp" #include "RunGui.hpp" +#include "common/Args.hpp" +#include "common/Modes.hpp" +#include "common/Version.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" #include "util/IncognitoBrowser.hpp" -#include -#include -#include - using namespace chatterino; int main(int argc, char **argv) @@ -18,17 +23,49 @@ int main(int argc, char **argv) auto args = QStringList(); std::transform(argv + 1, argv + argc, std::back_inserter(args), [&](auto s) { return s; }); + initArgs(args); // run in gui mode or browser extension host mode if (shouldRunBrowserExtensionHost(args)) { runBrowserExtensionHost(); } + else if (getArgs().printVersion) + { + qInfo().noquote() << Version::instance().fullVersion(); + } else { - Paths paths; - Settings settings(paths.settingsDirectory); + Paths *paths{}; - runGui(a, paths, settings); + try + { + paths = new Paths; + } + catch (std::runtime_error &error) + { + QMessageBox box; + if (Modes::instance().isPortable) + { + box.setText( + error.what() + + QStringLiteral( + "\n\nInfo: Portable mode requires the application to " + "be in a writeable location. If you don't want " + "portable mode reinstall the application. " + "https://chatterino.com.")); + } + else + { + box.setText(error.what()); + } + box.exec(); + return 1; + } + + Settings settings(paths->settingsDirectory); + + runGui(a, *paths, settings); } + return 0; } diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 675e7d15..558ffea2 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -1,5 +1,14 @@ #include "messages/Image.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + #include "Application.hpp" #include "common/Common.hpp" #include "common/NetworkRequest.hpp" @@ -11,15 +20,6 @@ #include "util/DebugCount.hpp" #include "util/PostToThread.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - namespace chatterino { namespace detail { // Frames @@ -198,6 +198,15 @@ namespace detail { } // namespace detail // IMAGE2 +Image::~Image() +{ + // run destructor of Frames in gui thread + if (!isGuiThread()) + { + postToThread([frames = this->frames_.release()]() { delete frames; }); + } +} + ImagePtr Image::fromUrl(const Url &url, qreal scale) { static std::unordered_map> cache; @@ -324,7 +333,7 @@ int Image::width() const assertInGuiThread(); if (auto pixmap = this->frames_->first()) - return pixmap->width() * this->scale_; + return int(pixmap->width() * this->scale_); else return 16; } @@ -369,6 +378,7 @@ void Image::actuallyLoad() if (!shared) return false; + // fourtf: is this the right thing to do? shared->empty_ = true; return true; diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp index 1cd30d81..f114d1f0 100644 --- a/src/messages/Image.hpp +++ b/src/messages/Image.hpp @@ -13,7 +13,7 @@ #include #include "common/Aliases.hpp" -#include "common/NullablePtr.hpp" +#include "common/Common.hpp" namespace chatterino { namespace detail { @@ -45,9 +45,12 @@ namespace detail { class Image; using ImagePtr = std::shared_ptr; +/// This class is thread safe. class Image : public std::enable_shared_from_this, boost::noncopyable { public: + ~Image(); + static ImagePtr fromUrl(const Url &url, qreal scale = 1); static ImagePtr fromPixmap(const QPixmap &pixmap, qreal scale = 1); static ImagePtr getEmpty(); @@ -72,14 +75,14 @@ private: Image(qreal scale); void setPixmap(const QPixmap &pixmap); - void actuallyLoad(); - Url url_{}; - qreal scale_{1}; - bool empty_{false}; + const Url url_{}; + const qreal scale_{1}; + std::atomic_bool empty_{false}; + + // gui thread only bool shouldLoad_{false}; std::unique_ptr frames_{}; - QObject object_{}; }; } // namespace chatterino diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index c38bf024..6e4b8115 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -32,9 +32,8 @@ std::pair makeAutomodMessage( builder.message().flags.set(MessageFlag::PubSub); builder - .emplace( - Image::fromPixmap(getApp()->resources->twitch.automod), - MessageElementFlag::BadgeChannelAuthority) + .emplace(Image::fromPixmap(getResources().twitch.automod), + MessageElementFlag::BadgeChannelAuthority) ->setTooltip("AutoMod"); builder.emplace("AutoMod:", MessageElementFlag::BoldUsername, MessageColor(QColor("blue")), @@ -258,40 +257,35 @@ MessageBuilder::MessageBuilder(const AutomodUserAction &action) QString text; switch (action.type) { - case AutomodUserAction::AddPermitted: - { + case AutomodUserAction::AddPermitted: { text = QString("%1 added %2 as a permitted term on AutoMod.") .arg(action.source.name) .arg(action.message); } break; - case AutomodUserAction::AddBlocked: - { + case AutomodUserAction::AddBlocked: { text = QString("%1 added %2 as a blocked term on AutoMod.") .arg(action.source.name) .arg(action.message); } break; - case AutomodUserAction::RemovePermitted: - { + case AutomodUserAction::RemovePermitted: { text = QString("%1 removed %2 as a permitted term term on AutoMod.") .arg(action.source.name) .arg(action.message); } break; - case AutomodUserAction::RemoveBlocked: - { + case AutomodUserAction::RemoveBlocked: { text = QString("%1 removed %2 as a blocked term on AutoMod.") .arg(action.source.name) .arg(action.message); } break; - case AutomodUserAction::Properties: - { + case AutomodUserAction::Properties: { text = QString("%1 modified the AutoMod properties.") .arg(action.source.name); } diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 0e18f6a2..7d1a00c6 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -103,6 +103,10 @@ enum class MessageElementFlag { LowercaseLink = (1 << 29), OriginalLink = (1 << 30), + // ZeroWidthEmotes are emotes that are supposed to overlay over any pre-existing emotes + // e.g. BTTV's SoSnowy during christmas season + ZeroWidthEmote = (1 << 31), + Default = Timestamp | Badges | Username | BitsStatic | FfzEmoteImage | BttvEmoteImage | TwitchEmoteImage | BitsAmount | Text | AlwaysShow, diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 07b7c79d..bf71c0f8 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -158,7 +158,7 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex, // create new buffer if required if (!pixmap) { -#ifdef Q_OS_MACOS +#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX) pixmap = new QPixmap(int(width * painter.device()->devicePixelRatioF()), int(container_->getHeight() * painter.device()->devicePixelRatioF())); diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp index f26eb52f..9c46e0c2 100644 --- a/src/messages/layouts/MessageLayoutContainer.cpp +++ b/src/messages/layouts/MessageLayoutContainer.cpp @@ -115,15 +115,27 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element, // update line height this->lineHeight_ = std::max(this->lineHeight_, newLineHeight); + auto xOffset = 0; + + if (element->getCreator().getFlags().has( + MessageElementFlag::ZeroWidthEmote)) + { + xOffset -= element->getRect().width() + this->spaceWidth_; + } + // set move element - element->setPosition( - QPoint(this->currentX_, this->currentY_ - element->getRect().height())); + element->setPosition(QPoint(this->currentX_ + xOffset, + this->currentY_ - element->getRect().height())); // add element this->elements_.push_back(std::unique_ptr(element)); // set current x - this->currentX_ += element->getRect().width(); + if (!element->getCreator().getFlags().has( + MessageElementFlag::ZeroWidthEmote)) + { + this->currentX_ += element->getRect().width(); + } if (element->hasTrailingSpace()) { @@ -137,7 +149,9 @@ void MessageLayoutContainer::breakLine() if (this->flags_.has(MessageFlag::Centered) && this->elements_.size() > 0) { - xOffset = (width_ - this->elements_.at(0)->getRect().left() - + const int marginOffset = int(this->margin.left * this->scale_) + + int(this->margin.right * this->scale_); + xOffset = (width_ - marginOffset - this->elements_.at(this->elements_.size() - 1) ->getRect() .right()) / diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 6b7c9ee2..36481dd7 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -1,5 +1,8 @@ #include "providers/bttv/BttvEmotes.hpp" +#include +#include + #include "common/Common.hpp" #include "common/NetworkRequest.hpp" #include "debug/Log.hpp" @@ -8,11 +11,11 @@ #include "messages/ImageSet.hpp" #include "providers/twitch/TwitchChannel.hpp" -#include -#include - namespace chatterino { namespace { + + QString emoteLinkFormat("https://betterttv.com/emotes/%1"); + Url getEmoteLink(QString urlTemplate, const EmoteId &id, const QString &emoteScale) { @@ -47,13 +50,14 @@ namespace { auto name = EmoteName{jsonEmote.toObject().value("code").toString()}; - auto emote = Emote( - {name, - ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), - Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), - Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)}, - Tooltip{name.string + "
Global BetterTTV Emote"}, - Url{"https://manage.betterttv.net/emotes/" + id.string}}); + auto emote = Emote({ + name, + ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), + Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), + Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)}, + Tooltip{name.string + "
Global BetterTTV Emote"}, + Url{emoteLinkFormat.arg(id.string)}, + }); emotes[name] = cachedOrMakeEmotePtr(std::move(emote), currentEmotes); @@ -75,15 +79,16 @@ namespace { auto name = EmoteName{jsonEmote.value("code").toString()}; // emoteObject.value("imageType").toString(); - auto emote = Emote( - {name, - ImageSet{ - Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), - Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), - Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25), - }, - Tooltip{name.string + "
Channel BetterTTV Emote"}, - Url{"https://manage.betterttv.net/emotes/" + id.string}}); + auto emote = Emote({ + name, + ImageSet{ + Image::fromUrl(getEmoteLinkV3(id, "1x"), 1), + Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5), + Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25), + }, + Tooltip{name.string + "
Channel BetterTTV Emote"}, + Url{emoteLinkFormat.arg(id.string)}, + }); emotes[name] = cachedOrMake(std::move(emote), id); } diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp index 9b17d301..20d78249 100644 --- a/src/providers/irc/AbstractIrcServer.hpp +++ b/src/providers/irc/AbstractIrcServer.hpp @@ -1,13 +1,13 @@ #pragma once -#include "providers/irc/IrcConnection2.hpp" - #include +#include +#include #include #include -#include -#include +#include "common/Common.hpp" +#include "providers/irc/IrcConnection2.hpp" namespace chatterino { @@ -73,15 +73,8 @@ protected: private: void initConnection(); - struct Deleter { - void operator()(IrcConnection *conn) - { - conn->deleteLater(); - } - }; - - std::unique_ptr writeConnection_ = nullptr; - std::unique_ptr readConnection_ = nullptr; + QObjectPtr writeConnection_ = nullptr; + QObjectPtr readConnection_ = nullptr; QTimer reconnectTimer_; int falloffCounter_ = 1; diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp index 7409ac98..0a8f2ed6 100644 --- a/src/providers/irc/Irc2.cpp +++ b/src/providers/irc/Irc2.cpp @@ -73,13 +73,13 @@ inline QString getCredentialName(const IrcServerData &data) void IrcServerData::getPassword( QObject *receiver, std::function &&onLoaded) const { - Credentials::getInstance().get("irc", getCredentialName(*this), receiver, - std::move(onLoaded)); + Credentials::instance().get("irc", getCredentialName(*this), receiver, + std::move(onLoaded)); } void IrcServerData::setPassword(const QString &password) { - Credentials::getInstance().set("irc", getCredentialName(*this), password); + Credentials::instance().set("irc", getCredentialName(*this), password); } Irc::Irc() @@ -133,8 +133,7 @@ Irc::Irc() if (args.caller != Irc::noEraseCredentialCaller) { - Credentials::getInstance().erase("irc", - getCredentialName(args.item)); + Credentials::instance().erase("irc", getCredentialName(args.item)); } }); @@ -164,7 +163,7 @@ ChannelPtr Irc::getOrAddChannel(int id, QString name) } } -Irc &Irc::getInstance() +Irc &Irc::instance() { static Irc irc; return irc; diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp index 0d40f6fe..de484924 100644 --- a/src/providers/irc/Irc2.hpp +++ b/src/providers/irc/Irc2.hpp @@ -36,7 +36,7 @@ class Irc public: Irc(); - static Irc &getInstance(); + static Irc &instance(); static inline void *const noEraseCredentialCaller = reinterpret_cast(1); diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp index 2266996c..dd7505ff 100644 --- a/src/providers/irc/IrcServer.cpp +++ b/src/providers/irc/IrcServer.cpp @@ -66,26 +66,29 @@ void IrcServer::initializeConnection(IrcConnection *connection, connection->setRealName(this->data_->real.isEmpty() ? this->data_->user : this->data_->nick); - switch (this->data_->authType) + if (getSettings()->enableExperimentalIrc) { - case IrcAuthType::Sasl: - connection->setSaslMechanism("PLAIN"); - [[fallthrough]]; - case IrcAuthType::Pass: - this->data_->getPassword( - this, [conn = new QObjectRef(connection) /* can't copy */, - this](const QString &password) mutable { - if (*conn) - { - (*conn)->setPassword(password); - this->open(Both); - } + switch (this->data_->authType) + { + case IrcAuthType::Sasl: + connection->setSaslMechanism("PLAIN"); + [[fallthrough]]; + case IrcAuthType::Pass: + this->data_->getPassword( + this, [conn = new QObjectRef(connection) /* can't copy */, + this](const QString &password) mutable { + if (*conn) + { + (*conn)->setPassword(password); + this->open(Both); + } - delete conn; - }); - break; - default: - this->open(Both); + delete conn; + }); + break; + default: + this->open(Both); + } } QObject::connect( @@ -179,8 +182,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) switch (message->type()) { - case Communi::IrcMessage::Join: - { + case Communi::IrcMessage::Join: { auto x = static_cast(message); if (auto it = @@ -205,8 +207,7 @@ void IrcServer::readConnectionMessageReceived(Communi::IrcMessage *message) return; } - case Communi::IrcMessage::Part: - { + case Communi::IrcMessage::Part: { auto x = static_cast(message); if (auto it = diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 3586c2a3..a3c5e3c7 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -40,7 +40,7 @@ static QMap parseBadges(QString badgesString) return badges; } -IrcMessageHandler &IrcMessageHandler::getInstance() +IrcMessageHandler &IrcMessageHandler::instance() { static IrcMessageHandler instance; return instance; @@ -618,7 +618,7 @@ void IrcMessageHandler::handlePartMessage(Communi::IrcMessage *message) { if (message->nick() != getApp()->accounts->twitch.getCurrent()->getUserName() && - getSettings()->showJoins.getValue()) + getSettings()->showParts.getValue()) { twitchChannel->addPartedUser(message->nick()); } diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index c0bc67d5..fe8321ad 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -13,7 +13,7 @@ class IrcMessageHandler IrcMessageHandler() = default; public: - static IrcMessageHandler &getInstance(); + static IrcMessageHandler &instance(); // parseMessage parses a single IRC message into 0+ Chatterino messages std::vector parseMessage(Channel *channel, diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 0e9c415a..0f8ad1d2 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -156,7 +156,7 @@ namespace detail { if (self->awaitingPong_) { - log("No pong respnose, disconnect!"); + log("No pong response, disconnect!"); // TODO(pajlada): Label this connection as "disconnect // me" } diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index f201e9db..b37a67da 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -102,14 +102,12 @@ void TwitchAccountManager::reloadUsers() switch (this->addUser(userData)) { - case AddUserResponse::UserAlreadyExists: - { + case AddUserResponse::UserAlreadyExists: { log("User {} already exists", userData.username); // Do nothing } break; - case AddUserResponse::UserValuesUpdated: - { + case AddUserResponse::UserValuesUpdated: { log("User {} already exists, and values updated!", userData.username); if (userData.username == this->getCurrent()->getUserName()) @@ -120,8 +118,7 @@ void TwitchAccountManager::reloadUsers() } } break; - case AddUserResponse::UserAdded: - { + case AddUserResponse::UserAdded: { log("Added user {}", userData.username); listUpdated = true; } diff --git a/src/providers/twitch/TwitchBadge.cpp b/src/providers/twitch/TwitchBadge.cpp new file mode 100644 index 00000000..699e4b87 --- /dev/null +++ b/src/providers/twitch/TwitchBadge.cpp @@ -0,0 +1,31 @@ +#include "providers/twitch/TwitchBadge.hpp" + +#include + +namespace chatterino { + +// set of badge IDs that should be given specific flags. +// vanity flag is left out on purpose as it is our default flag +const QSet globalAuthority{"staff", "admin", "global_mod"}; +const QSet channelAuthority{"moderator", "vip", "broadcaster"}; +const QSet subBadges{"subscriber", "founder"}; + +Badge::Badge(QString key, QString value) + : key_(std::move(key)) + , value_(std::move(value)) +{ + if (globalAuthority.contains(this->key_)) + { + this->flag_ = MessageElementFlag::BadgeGlobalAuthority; + } + else if (channelAuthority.contains(this->key_)) + { + this->flag_ = MessageElementFlag::BadgeChannelAuthority; + } + else if (subBadges.contains(this->key_)) + { + this->flag_ = MessageElementFlag::BadgeSubscription; + } +} + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchBadge.hpp b/src/providers/twitch/TwitchBadge.hpp new file mode 100644 index 00000000..764be2e7 --- /dev/null +++ b/src/providers/twitch/TwitchBadge.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "messages/MessageElement.hpp" + +#include + +namespace chatterino { + +class Badge +{ +public: + Badge(QString key, QString value); + + QString key_; // e.g. bits + QString value_; // e.g. 100 + QString extraValue_{}; // e.g. 5 (the number of months subscribed) + MessageElementFlag flag_{ + MessageElementFlag::BadgeVanity}; // badge slot it takes up +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index 84d73311..cc9022a4 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -47,7 +47,7 @@ void TwitchBadges::loadTwitchBadges() {versionObj.value("image_url_4x").toString()}, .25), }, - Tooltip{versionObj.value("description").toString()}, + Tooltip{versionObj.value("title").toString()}, Url{versionObj.value("click_url").toString()}}; // "title" // "clickAction" diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 907bdaf0..27fe8bd3 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -31,6 +31,7 @@ namespace chatterino { namespace { + constexpr int TITLE_REFRESH_PERIOD = 10; constexpr char MAGIC_MESSAGE_SUFFIX[] = u8" \U000E0000"; // parseRecentMessages takes a json object and returns a vector of @@ -89,6 +90,7 @@ TwitchChannel::TwitchChannel(const QString &name, , bttvEmotes_(std::make_shared()) , ffzEmotes_(std::make_shared()) , mod_(false) + , titleRefreshedTime_(QTime::currentTime().addSecs(-TITLE_REFRESH_PERIOD)) { log("[TwitchChannel:{}] Opened", name); @@ -110,6 +112,7 @@ TwitchChannel::TwitchChannel(const QString &name, // room id loaded -> refresh live status this->roomIdChanged.connect([this]() { this->refreshPubsub(); + this->refreshTitle(); this->refreshLiveStatus(); this->refreshBadges(); this->refreshCheerEmotes(); @@ -229,7 +232,7 @@ bool TwitchChannel::isMod() const return this->mod_; } -bool TwitchChannel::isVIP() const +bool TwitchChannel::isVip() const { return this->vip_; } @@ -278,7 +281,7 @@ bool TwitchChannel::isBroadcaster() const bool TwitchChannel::hasHighRateLimit() const { - return this->isMod() || this->isBroadcaster() || this->isVIP(); + return this->isMod() || this->isBroadcaster() || this->isVip(); } bool TwitchChannel::canReconnect() const @@ -437,6 +440,51 @@ void TwitchChannel::setLive(bool newLiveStatus) } } +void TwitchChannel::refreshTitle() +{ + auto roomID = this->roomId(); + if (roomID.isEmpty()) + { + return; + } + + if (this->titleRefreshedTime_.elapsed() < TITLE_REFRESH_PERIOD * 1000) + { + return; + } + this->titleRefreshedTime_ = QTime::currentTime(); + + QString url("https://api.twitch.tv/kraken/channels/" + roomID); + NetworkRequest::twitchRequest(url) + .onSuccess( + [this, weak = weakOf(this)](auto result) -> Outcome { + ChannelPtr shared = weak.lock(); + if (!shared) + return Failure; + + const auto document = result.parseRapidJson(); + + auto statusIt = document.FindMember("status"); + + if (statusIt == document.MemberEnd()) + { + return Failure; + } + + { + auto status = this->streamStatus_.access(); + if (!rj::getSafe(statusIt->value, status->title)) + { + return Failure; + } + } + + this->liveStatusChanged.invoke(); + return Success; + }) + .execute(); +} + void TwitchChannel::refreshLiveStatus() { auto roomID = this->roomId(); @@ -566,7 +614,7 @@ void TwitchChannel::loadRecentMessages() auto messages = parseRecentMessages(result.parseJson(), shared); - auto &handler = IrcMessageHandler::getInstance(); + auto &handler = IrcMessageHandler::instance(); std::vector allBuiltMessages; @@ -711,6 +759,7 @@ void TwitchChannel::refreshCheerEmotes() cheerEmote.color = QColor(tier.color); cheerEmote.minBits = tier.minBits; + cheerEmote.regex = cheerEmoteSet.regex; // TODO(pajlada): We currently hardcode dark here :| // We will continue to do so for now since we haven't had to diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 0d754ec1..026c9071 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -63,12 +63,13 @@ public: virtual bool canSendMessage() const override; virtual void sendMessage(const QString &message) override; virtual bool isMod() const override; - bool isVIP() const; + bool isVip() const; bool isStaff() const; virtual bool isBroadcaster() const override; virtual bool hasHighRateLimit() const override; virtual bool canReconnect() const override; virtual void reconnect() override; + void refreshTitle(); // Data const QString &subscriptionUrl(); @@ -166,6 +167,7 @@ private: QObject lifetimeGuard_; QTimer liveStatusTimer_; QTimer chattersListTimer_; + QTime titleRefreshedTime_; friend class TwitchIrcServer; friend class TwitchMessageBuilder; diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index 808d86db..d0986cfe 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -18,6 +18,7 @@ using EmotePtr = std::shared_ptr; struct CheerEmote { QColor color; int minBits; + QRegularExpression regex; EmotePtr animatedEmote; EmotePtr staticEmote; diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index de0e9efb..45f50cec 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -1,7 +1,11 @@ #include "TwitchIrcServer.hpp" +#include +#include + #include "Application.hpp" #include "common/Common.hpp" +#include "common/Env.hpp" #include "controllers/accounts/AccountController.hpp" #include "controllers/highlights/HighlightController.hpp" #include "messages/Message.hpp" @@ -15,9 +19,6 @@ #include "providers/twitch/TwitchMessageBuilder.hpp" #include "util/PostToThread.hpp" -#include -#include - // using namespace Communi; using namespace std::chrono_literals; @@ -86,13 +87,12 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection, connection->setPassword(oauthToken); } - connection->setSecure(true); - // https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc - // SSL disabled: irc://irc.chat.twitch.tv:6667 - // SSL enabled: irc://irc.chat.twitch.tv:6697 - connection->setHost("irc.chat.twitch.tv"); - connection->setPort(6697); + // SSL disabled: irc://irc.chat.twitch.tv:6667 (or port 80) + // SSL enabled: irc://irc.chat.twitch.tv:6697 (or port 443) + connection->setHost(Env::get().twitchServerHost); + connection->setPort(Env::get().twitchServerPort); + connection->setSecure(Env::get().twitchServerSecure); this->open(type); } @@ -125,7 +125,7 @@ std::shared_ptr TwitchIrcServer::createChannel( void TwitchIrcServer::privateMessageReceived( Communi::IrcPrivateMessage *message) { - IrcMessageHandler::getInstance().handlePrivMessage(message, *this); + IrcMessageHandler::instance().handlePrivMessage(message, *this); } void TwitchIrcServer::readConnectionMessageReceived( @@ -141,7 +141,7 @@ void TwitchIrcServer::readConnectionMessageReceived( const QString &command = message->command(); - auto &handler = IrcMessageHandler::getInstance(); + auto &handler = IrcMessageHandler::instance(); // Below commands enabled through the twitch.tv/membership CAP REQ if (command == "MODE") @@ -194,14 +194,41 @@ void TwitchIrcServer::writeConnectionMessageReceived( { const QString &command = message->command(); - auto &handler = IrcMessageHandler::getInstance(); - + auto &handler = IrcMessageHandler::instance(); // Below commands enabled through the twitch.tv/commands CAP REQ if (command == "USERSTATE") { // Received USERSTATE upon PRIVMSGing handler.handleUserStateMessage(message); } + else if (command == "NOTICE") + { + static std::unordered_set readConnectionOnlyIDs{ + "host_on", + "host_off", + "host_target_went_offline", + "emote_only_on", + "emote_only_off", + "slow_on", + "slow_off", + "subs_on", + "subs_off", + "r9k_on", + "r9k_off", + + // Display for user who times someone out. This implies you're a + // moderator, at which point you will be connected to PubSub and receive + // a better message from there. + "timeout_success", + "ban_success", + + // Channel suspended notices + "msg_channel_suspended", + }; + + handler.handleNoticeMessage( + static_cast(message)); + } } void TwitchIrcServer::onReadConnected(IrcConnection *connection) diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 3f94877a..b781061e 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -28,6 +28,10 @@ namespace { +const QSet zeroWidthEmotes{ + "SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane", +}; + QColor getRandomColor(const QVariant &userId) { static const std::vector twitchUsernameColors = { @@ -65,6 +69,59 @@ QColor getRandomColor(const QVariant &userId) namespace chatterino { +namespace { + + QStringList parseTagList(const QVariantMap &tags, const QString &key) + { + auto iterator = tags.find(key); + if (iterator == tags.end()) + return QStringList{}; + + return iterator.value().toString().split( + ',', QString::SplitBehavior::SkipEmptyParts); + } + + std::map parseBadgeInfos(const QVariantMap &tags) + { + std::map badgeInfos; + + for (QString badgeInfo : parseTagList(tags, "badge-info")) + { + QStringList parts = badgeInfo.split('/'); + if (parts.size() != 2) + { + log("Skipping badge-info because it split weird: {}", + badgeInfo); + continue; + } + + badgeInfos.emplace(parts[0], parts[1]); + } + + return badgeInfos; + } + + std::vector parseBadges(const QVariantMap &tags) + { + std::vector badges; + + for (QString badge : parseTagList(tags, "badges")) + { + QStringList parts = badge.split('/'); + if (parts.size() != 2) + { + log("Skipping badge because it split weird: {}", badge); + continue; + } + + badges.emplace_back(parts[0], parts[1]); + } + + return badges; + } + +} // namespace + TwitchMessageBuilder::TwitchMessageBuilder( Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, const MessageParseArgs &_args) @@ -289,6 +346,7 @@ MessagePtr TwitchMessageBuilder::build() if (iterator != this->tags.end()) { this->hasBits_ = true; + this->bitsLeft = iterator.value().toInt(); this->bits = iterator.value().toString(); } @@ -617,14 +675,12 @@ void TwitchMessageBuilder::appendUsername() switch (usernameDisplayMode.getValue()) { - case UsernameDisplayMode::Username: - { + case UsernameDisplayMode::Username: { usernameText = username; } break; - case UsernameDisplayMode::LocalizedName: - { + case UsernameDisplayMode::LocalizedName: { if (hasLocalizedName) { usernameText = localizedName; @@ -637,8 +693,7 @@ void TwitchMessageBuilder::appendUsername() break; default: - case UsernameDisplayMode::UsernameAndLocalizedName: - { + case UsernameDisplayMode::UsernameAndLocalizedName: { if (hasLocalizedName) { usernameText = username + "(" + localizedName + ")"; @@ -655,7 +710,7 @@ void TwitchMessageBuilder::appendUsername() { // TODO(pajlada): Re-implement // userDisplayString += - // IrcManager::getInstance().getUser().getUserName(); + // IrcManager::instance().getUser().getUserName(); } else if (this->args.isReceivedWhisper) { @@ -1112,6 +1167,11 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) else if ((emote = globalBttvEmotes.emote(name))) { flags = MessageElementFlag::BttvEmote; + + if (zeroWidthEmotes.contains(name.string)) + { + flags.set(MessageElementFlag::ZeroWidthEmote); + } } if (emote) @@ -1123,7 +1183,24 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name) return Failure; } -// fourtf: this is ugly +boost::optional TwitchMessageBuilder::getTwitchBadge( + const Badge &badge) +{ + if (auto channelBadge = + this->twitchChannel->twitchBadge(badge.key_, badge.value_)) + { + return channelBadge; + } + + if (auto globalBadge = this->twitchChannel->globalTwitchBadges().badge( + badge.key_, badge.value_)) + { + return globalBadge; + } + + return boost::none; +} + void TwitchMessageBuilder::appendTwitchBadges() { if (this->twitchChannel == nullptr) @@ -1131,68 +1208,25 @@ void TwitchMessageBuilder::appendTwitchBadges() return; } - auto app = getApp(); + auto badgeInfos = parseBadgeInfos(this->tags); + auto badges = parseBadges(this->tags); - auto iterator = this->tags.find("badges"); - if (iterator == this->tags.end()) - return; - - for (QString badge : iterator.value().toString().split(',')) + for (const auto &badge : badges) { - if (badge.startsWith("bits/")) + auto badgeEmote = this->getTwitchBadge(badge); + if (!badgeEmote) { - QString cheerAmount = badge.mid(5); - QString tooltip = QString("Twitch cheer ") + cheerAmount; + log("No channel/global variant found {}", badge.key_); + continue; + } + auto tooltip = (*badgeEmote)->tooltip.string; - // Try to fetch channel-specific bit badge - try - { - if (twitchChannel) - if (const auto &_badge = this->twitchChannel->twitchBadge( - "bits", cheerAmount)) - { - this->emplace( - _badge.get(), MessageElementFlag::BadgeVanity) - ->setTooltip(tooltip); - continue; - } - } - catch (const std::out_of_range &) - { - // Channel does not contain a special bit badge for this version - } - - // Use default bit badge - if (auto _badge = this->twitchChannel->globalTwitchBadges().badge( - "bits", cheerAmount)) - { - this->emplace(_badge.get(), - MessageElementFlag::BadgeVanity) - ->setTooltip(tooltip); - } - } - else if (badge == "staff/1") + if (badge.key_ == "bits") { - this->emplace( - Image::fromPixmap(app->resources->twitch.staff), - MessageElementFlag::BadgeGlobalAuthority) - ->setTooltip("Twitch Staff"); + const auto &cheerAmount = badge.value_; + tooltip = QString("Twitch cheer %0").arg(cheerAmount); } - else if (badge == "admin/1") - { - this->emplace( - Image::fromPixmap(app->resources->twitch.admin), - MessageElementFlag::BadgeGlobalAuthority) - ->setTooltip("Twitch Admin"); - } - else if (badge == "global_mod/1") - { - this->emplace( - Image::fromPixmap(app->resources->twitch.globalmod), - MessageElementFlag::BadgeGlobalAuthority) - ->setTooltip("Twitch Global Moderator"); - } - else if (badge == "moderator/1") + else if (badge.key_ == "moderator") { if (auto customModBadge = this->twitchChannel->ffzCustomModBadge()) { @@ -1200,104 +1234,22 @@ void TwitchMessageBuilder::appendTwitchBadges() customModBadge.get(), MessageElementFlag::BadgeChannelAuthority) ->setTooltip((*customModBadge)->tooltip.string); + // early out, since we have to add a custom badge element here continue; } - this->emplace( - Image::fromPixmap(app->resources->twitch.moderator), - MessageElementFlag::BadgeChannelAuthority) - ->setTooltip("Twitch Channel Moderator"); } - else if (badge == "vip/1") + else if (badge.flag_ == MessageElementFlag::BadgeSubscription) { - this->emplace( - Image::fromPixmap(app->resources->twitch.vip), - MessageElementFlag::BadgeChannelAuthority) - ->setTooltip("VIP"); - } - else if (badge == "broadcaster/1") - { - this->emplace( - Image::fromPixmap(app->resources->twitch.broadcaster), - MessageElementFlag::BadgeChannelAuthority) - ->setTooltip("Twitch Broadcaster"); - } - else if (badge == "turbo/1") - { - this->emplace( - Image::fromPixmap(app->resources->twitch.turbo), - MessageElementFlag::BadgeVanity) - ->setTooltip("Twitch Turbo Subscriber"); - } - else if (badge == "premium/1") - { - this->emplace( - Image::fromPixmap(app->resources->twitch.prime), - MessageElementFlag::BadgeVanity) - ->setTooltip("Twitch Prime Subscriber"); - } - else if (badge.startsWith("partner/")) - { - int index = badge.midRef(8).toInt(); - switch (index) + auto badgeInfoIt = badgeInfos.find(badge.key_); + if (badgeInfoIt != badgeInfos.end()) { - case 1: - { - this->emplace( - Image::fromPixmap(app->resources->twitch.verified, - 0.25), - MessageElementFlag::BadgeVanity) - ->setTooltip("Twitch Verified"); - } - break; - default: - { - printf("[TwitchMessageBuilder] Unhandled partner badge " - "index: %d\n", - index); - } - break; + const auto &subMonths = badgeInfoIt->second; + tooltip += QString(" (%0 months)").arg(subMonths); } } - else if (badge.startsWith("subscriber/")) - { - if (auto badgeEmote = this->twitchChannel->twitchBadge( - "subscriber", badge.mid(11))) - { - this->emplace( - badgeEmote.get(), MessageElementFlag::BadgeSubscription) - ->setTooltip((*badgeEmote)->tooltip.string); - continue; - } - // use default subscriber badge if custom one not found - this->emplace( - Image::fromPixmap(app->resources->twitch.subscriber, 0.25), - MessageElementFlag::BadgeSubscription) - ->setTooltip("Twitch Subscriber"); - } - else - { - auto splits = badge.split('/'); - if (splits.size() != 2) - continue; - - if (auto badgeEmote = - this->twitchChannel->twitchBadge(splits[0], splits[1])) - { - this->emplace(badgeEmote.get(), - MessageElementFlag::BadgeVanity) - ->setTooltip((*badgeEmote)->tooltip.string); - continue; - } - if (auto _badge = this->twitchChannel->globalTwitchBadges().badge( - splits[0], splits[1])) - { - this->emplace(_badge.get(), - MessageElementFlag::BadgeVanity) - ->setTooltip((*_badge)->tooltip.string); - continue; - } - } + this->emplace(badgeEmote.get(), badge.flag_) + ->setTooltip(tooltip); } } @@ -1312,12 +1264,67 @@ void TwitchMessageBuilder::appendChatterinoBadges() Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) { + if (this->bitsLeft == 0) + { + return Failure; + } + auto cheerOpt = this->twitchChannel->cheerEmote(string); + if (!cheerOpt) { return Failure; } + auto &cheerEmote = *cheerOpt; + auto match = cheerEmote.regex.match(string); + + if (!match.hasMatch()) + { + return Failure; + } + + int cheerValue = match.captured(1).toInt(); + + if (getSettings()->stackBits) + { + if (this->bitsStacked) + { + return Success; + } + if (cheerEmote.staticEmote) + { + this->emplace(cheerEmote.staticEmote, + MessageElementFlag::BitsStatic); + } + if (cheerEmote.animatedEmote) + { + this->emplace(cheerEmote.animatedEmote, + MessageElementFlag::BitsAnimated); + } + if (cheerEmote.color != QColor()) + { + this->emplace(QString::number(this->bitsLeft), + MessageElementFlag::BitsAmount, + cheerEmote.color); + } + this->bitsStacked = true; + return Success; + } + + if (this->bitsLeft >= cheerValue) + { + this->bitsLeft -= cheerValue; + } + else + { + QString newString = string; + newString.chop(QString::number(cheerValue).length()); + newString += QString::number(cheerValue - this->bitsLeft); + + return tryParseCheermote(newString); + } + if (cheerEmote.staticEmote) { this->emplace(cheerEmote.staticEmote, @@ -1330,9 +1337,12 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string) } if (cheerEmote.color != QColor()) { - this->emplace(this->bits, MessageElementFlag::BitsAmount, + this->emplace(match.captured(1), + MessageElementFlag::BitsAmount, cheerEmote.color); } + return Success; } + } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index c43c1419..e85fce95 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -3,6 +3,7 @@ #include "common/Aliases.hpp" #include "common/Outcome.hpp" #include "messages/MessageBuilder.hpp" +#include "providers/twitch/TwitchBadge.hpp" #include #include @@ -60,6 +61,7 @@ private: // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function void parseHighlights(); + boost::optional getTwitchBadge(const Badge &badge); void appendTwitchEmote( const QString &emote, std::vector> &vec, @@ -79,6 +81,8 @@ private: QString roomID_; bool hasBits_ = false; QString bits; + int bitsLeft; + bool bitsStacked = false; bool historicalMessage_ = false; QString userId_; diff --git a/lib/appbase/singletons/Fonts.cpp b/src/singletons/Fonts.cpp similarity index 98% rename from lib/appbase/singletons/Fonts.cpp rename to src/singletons/Fonts.cpp index 2cc29b0b..521baefa 100644 --- a/lib/appbase/singletons/Fonts.cpp +++ b/src/singletons/Fonts.cpp @@ -24,7 +24,7 @@ # endif #endif -namespace AB_NAMESPACE { +namespace chatterino { namespace { int getBoldness() { @@ -89,7 +89,7 @@ void Fonts::initialize(Settings &, Paths &) }, false); #endif -} // namespace AB_NAMESPACE +} QFont Fonts::getFont(FontStyle type, float scale) { @@ -178,4 +178,4 @@ Fonts *getFonts() return Fonts::instance; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/singletons/Fonts.hpp b/src/singletons/Fonts.hpp similarity index 97% rename from lib/appbase/singletons/Fonts.hpp rename to src/singletons/Fonts.hpp index ad1fe8ca..bc700e5e 100644 --- a/lib/appbase/singletons/Fonts.hpp +++ b/src/singletons/Fonts.hpp @@ -12,7 +12,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { class Settings; class Paths; @@ -90,4 +90,4 @@ private: Fonts *getFonts(); -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index ea8e2526..efbe5a99 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -11,6 +11,8 @@ #include "common/Modes.hpp" #include "util/CombinePath.hpp" +using namespace std::literals; + namespace chatterino { Paths *Paths::instance = nullptr; @@ -22,7 +24,7 @@ Paths::Paths() this->initAppFilePathHash(); this->initCheckPortable(); - this->initAppDataDirectory(); + this->initRootDirectory(); this->initSubDirectories(); } @@ -33,7 +35,7 @@ bool Paths::createFolder(const QString &folderPath) bool Paths::isPortable() { - return Modes::getInstance().isPortable; + return Modes::instance().isPortable; } QString Paths::cacheDirectory() @@ -76,7 +78,7 @@ void Paths::initCheckPortable() combinePath(QCoreApplication::applicationDirPath(), "portable")); } -void Paths::initAppDataDirectory() +void Paths::initRootDirectory() { assert(this->portable_.is_initialized()); @@ -95,8 +97,8 @@ void Paths::initAppDataDirectory() QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); if (path.isEmpty()) { - throw std::runtime_error( - "Error finding writable location for settings"); + throw std::runtime_error("Could not create directory \""s + + path.toStdString() + "\""); } // create directory Chatterino2 instead of chatterino on windows because the @@ -123,8 +125,8 @@ void Paths::initSubDirectories() if (!QDir().mkpath(path)) { - throw std::runtime_error( - "Error creating appdata path %appdata%/chatterino/" + name); + throw std::runtime_error("Could not create directory \""s + + path.toStdString() + "\""); } return path; diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index 1df1fc77..ea4cb6c2 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -39,7 +39,7 @@ public: private: void initAppFilePathHash(); void initCheckPortable(); - void initAppDataDirectory(); + void initRootDirectory(); void initSubDirectories(); boost::optional portable_; diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index daec74a5..18995dc3 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -1 +1,24 @@ #include "singletons/Resources.hpp" + +#include "debug/AssertInGuiThread.hpp" + +namespace chatterino { +namespace { + static Resources2 *resources = nullptr; +} + +Resources2 &getResources() +{ + assert(resources); + + return *resources; +} + +void initResources() +{ + assertInGuiThread(); + + resources = new Resources2; +} + +} // namespace chatterino diff --git a/src/singletons/Resources.hpp b/src/singletons/Resources.hpp index 57a1b79a..f4217ca8 100644 --- a/src/singletons/Resources.hpp +++ b/src/singletons/Resources.hpp @@ -1,3 +1,12 @@ #pragma once #include "autogenerated/ResourcesAutogen.hpp" + +namespace chatterino { + +/// This class in thread safe but needs to be initialized from the gui thread +/// first. +Resources2 &getResources(); +void initResources(); + +} // namespace chatterino diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 9a077b09..94d13b68 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -9,12 +9,12 @@ namespace chatterino { -Settings *Settings::instance = nullptr; +Settings *Settings::instance_ = nullptr; Settings::Settings(const QString &settingsDirectory) : ABSettings(settingsDirectory) { - instance = this; + instance_ = this; #ifdef USEWINSDK this->autorun = isRegisteredForStartup(); @@ -23,14 +23,14 @@ Settings::Settings(const QString &settingsDirectory) #endif } -Settings &Settings::getInstance() +Settings &Settings::instance() { - return *instance; + return *instance_; } Settings *getSettings() { - return &Settings::getInstance(); + return &Settings::instance(); } } // namespace chatterino diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index a516cdd2..acdb495d 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -1,25 +1,24 @@ #pragma once -#include "BaseSettings.hpp" +#include +#include +#include "BaseSettings.hpp" #include "common/Channel.hpp" #include "controllers/highlights/HighlightPhrase.hpp" #include "controllers/moderationactions/ModerationAction.hpp" #include "singletons/Toasts.hpp" -#include -#include - namespace chatterino { class Settings : public ABSettings { - static Settings *instance; + static Settings *instance_; public: Settings(const QString &settingsDirectory); - static Settings &getInstance(); + static Settings &instance(); /// Appearance BoolSetting showTimestamps = {"/appearance/messages/showTimestamps", true}; @@ -121,6 +120,8 @@ public: QStringSetting emojiSet = {"/emotes/emojiSet", "EmojiOne 2"}; + BoolSetting stackBits = {"/emotes/stackBits", false}; + /// Links BoolSetting linksDoubleClickOnly = {"/links/doubleClickToOpen", false}; BoolSetting linkInfoTooltip = {"/links/linkInfoTooltip", false}; @@ -204,6 +205,7 @@ public: #ifdef Q_OS_LINUX BoolSetting useKeyring = {"/misc/useKeyring", true}; #endif + BoolSetting enableExperimentalIrc = {"/misc/experimentalIrc", false}; IntSetting startUpNotification = {"/misc/startUpNotification", 0}; QStringSetting currentVersion = {"/misc/currentVersion", ""}; @@ -213,6 +215,9 @@ public: BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0}; QStringSetting cachePath = {"/cache/path", ""}; + BoolSetting restartOnCrash = {"/misc/restartOnCrash", false}; + BoolSetting attachExtensionToAnyProcess = { + "/misc/attachExtensionToAnyProcess", false}; /// Debug BoolSetting showUnhandledIrcMessages = {"/debug/showUnhandledIrcMessages", diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index b99b42a5..4fb1fdd2 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -85,7 +85,7 @@ void Theme::actuallyUpdate(double hue, double multiplier) if (getSettings()->highlightColor != "") { this->messages.backgrounds.highlighted = - QColor(getSettings()->highlightColor); + QColor(getSettings()->highlightColor.getValue()); } } diff --git a/src/singletons/Toasts.cpp b/src/singletons/Toasts.cpp index e550b633..def5df00 100644 --- a/src/singletons/Toasts.cpp +++ b/src/singletons/Toasts.cpp @@ -134,8 +134,7 @@ public: } QDesktopServices::openUrl(QUrl(link)); break; - case ToastReaction::OpenInStreamlink: - { + case ToastReaction::OpenInStreamlink: { openStreamlinkForChannel(channelName_); break; } diff --git a/src/singletons/TooltipPreviewImage.cpp b/src/singletons/TooltipPreviewImage.cpp index 847cc3e6..5355b7d8 100644 --- a/src/singletons/TooltipPreviewImage.cpp +++ b/src/singletons/TooltipPreviewImage.cpp @@ -5,7 +5,7 @@ #include "widgets/TooltipWidget.hpp" namespace chatterino { -TooltipPreviewImage &TooltipPreviewImage::getInstance() +TooltipPreviewImage &TooltipPreviewImage::instance() { static TooltipPreviewImage *instance = new TooltipPreviewImage(); return *instance; @@ -14,7 +14,7 @@ TooltipPreviewImage &TooltipPreviewImage::getInstance() TooltipPreviewImage::TooltipPreviewImage() { connections_.push_back(getApp()->windows->gifRepaintRequested.connect([&] { - auto tooltipWidget = TooltipWidget::getInstance(); + auto tooltipWidget = TooltipWidget::instance(); if (this->image_ && !tooltipWidget->isHidden()) { auto pixmap = this->image_->pixmapOrLoad(); diff --git a/src/singletons/TooltipPreviewImage.hpp b/src/singletons/TooltipPreviewImage.hpp index 67ba9cd9..5b23945f 100644 --- a/src/singletons/TooltipPreviewImage.hpp +++ b/src/singletons/TooltipPreviewImage.hpp @@ -6,7 +6,7 @@ namespace chatterino { class TooltipPreviewImage { public: - static TooltipPreviewImage &getInstance(); + static TooltipPreviewImage &instance(); void setImage(ImagePtr image); TooltipPreviewImage(const TooltipPreviewImage &) = delete; diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index b5fcea2f..8f105498 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace chatterino { namespace { @@ -20,6 +21,38 @@ namespace { { return getSettings()->betaUpdates ? "beta" : "stable"; } + + /// Checks if the online version is newer or older than the current version. + bool isDowngradeOf(const QString &online, const QString ¤t) + { + static auto matchVersion = + QRegularExpression(R"((\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?)"); + + // Versions are just strings, they don't need to follow a specific + // format so we can only assume if one version is newer than another + // one. + + // We match x.x.x.x with each version level being optional. + + auto onlineMatch = matchVersion.match(online); + auto currentMatch = matchVersion.match(current); + + for (int i = 1; i <= 4; i++) + { + if (onlineMatch.captured(i).toInt() < + currentMatch.captured(i).toInt()) + { + return true; + } + if (onlineMatch.captured(i).toInt() > + currentMatch.captured(i).toInt()) + { + break; + } + } + + return false; + } } // namespace Updates::Updates() @@ -29,7 +62,7 @@ Updates::Updates() qDebug() << "init UpdateManager"; } -Updates &Updates::getInstance() +Updates &Updates::instance() { // fourtf: don't add this class to the application class static Updates instance; @@ -201,7 +234,7 @@ void Updates::checkForUpdates() { // Disable updates if on nightly and windows. #ifdef Q_OS_WIN - if (Modes::getInstance().isNightly) + if (Modes::instance().isNightly) { return; } @@ -263,6 +296,8 @@ void Updates::checkForUpdates() if (this->currentVersion_ != this->onlineVersion_) { this->setStatus_(UpdateAvailable); + this->isDowngrade_ = + isDowngradeOf(this->onlineVersion_, this->currentVersion_); } else { @@ -309,6 +344,11 @@ bool Updates::isError() const } } +bool Updates::isDowngrade() const +{ + return this->isDowngrade_; +} + void Updates::setStatus_(Status status) { if (this->status_ != status) diff --git a/src/singletons/Updates.hpp b/src/singletons/Updates.hpp index 377210ed..9ad33b28 100644 --- a/src/singletons/Updates.hpp +++ b/src/singletons/Updates.hpp @@ -22,7 +22,7 @@ public: }; // fourtf: don't add this class to the application class - static Updates &getInstance(); + static Updates &instance(); void checkForUpdates(); const QString &getCurrentVersion() const; @@ -32,6 +32,7 @@ public: bool shouldShowUpdateButton() const; bool isError() const; + bool isDowngrade() const; pajlada::Signals::Signal statusUpdated; @@ -39,6 +40,7 @@ private: QString currentVersion_; QString onlineVersion_; Status status_ = None; + bool isDowngrade_{}; QString updateExe_; QString updatePortable_; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index abafc90d..65bc8f44 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -1,5 +1,16 @@ #include "singletons/WindowManager.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "Application.hpp" #include "debug/AssertInGuiThread.hpp" #include "debug/Log.hpp" @@ -21,18 +32,6 @@ #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - #define SETTINGS_FILENAME "/window-layout.json" namespace chatterino { @@ -556,8 +555,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj) { switch (node->getType()) { - case SplitNode::_Split: - { + case SplitNode::_Split: { obj.insert("type", "split"); obj.insert("moderationMode", node->getSplit()->getModerationMode()); QJsonObject split; @@ -568,8 +566,7 @@ void WindowManager::encodeNodeRecusively(SplitNode *node, QJsonObject &obj) } break; case SplitNode::HorizontalContainer: - case SplitNode::VerticalContainer: - { + case SplitNode::VerticalContainer: { obj.insert("type", node->getType() == SplitNode::HorizontalContainer ? "horizontal" : "vertical"); @@ -593,29 +590,24 @@ void WindowManager::encodeChannel(IndirectChannel channel, QJsonObject &obj) switch (channel.getType()) { - case Channel::Type::Twitch: - { + case Channel::Type::Twitch: { obj.insert("type", "twitch"); obj.insert("name", channel.get()->getName()); } break; - case Channel::Type::TwitchMentions: - { + case Channel::Type::TwitchMentions: { obj.insert("type", "mentions"); } break; - case Channel::Type::TwitchWatching: - { + case Channel::Type::TwitchWatching: { obj.insert("type", "watching"); } break; - case Channel::Type::TwitchWhispers: - { + case Channel::Type::TwitchWhispers: { obj.insert("type", "whispers"); } break; - case Channel::Type::Irc: - { + case Channel::Type::Irc: { if (auto ircChannel = dynamic_cast(channel.get().get())) { @@ -657,8 +649,8 @@ IndirectChannel WindowManager::decodeChannel(const QJsonObject &obj) } else if (type == "irc") { - return Irc::getInstance().getOrAddChannel( - obj.value("server").toInt(-1), obj.value("channel").toString()); + return Irc::instance().getOrAddChannel(obj.value("server").toInt(-1), + obj.value("channel").toString()); } return Channel::getEmpty(); diff --git a/lib/appbase/util/Clamp.hpp b/src/util/Clamp.hpp similarity index 80% rename from lib/appbase/util/Clamp.hpp rename to src/util/Clamp.hpp index 0bb8febd..33030f70 100644 --- a/lib/appbase/util/Clamp.hpp +++ b/src/util/Clamp.hpp @@ -1,6 +1,6 @@ #pragma once -namespace AB_NAMESPACE { +namespace chatterino { // http://en.cppreference.com/w/cpp/algorithm/clamp @@ -10,4 +10,4 @@ constexpr const T &clamp(const T &v, const T &lo, const T &hi) return assert(!(hi < lo)), (v < lo) ? lo : (hi < v) ? hi : v; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/util/CombinePath.hpp b/src/util/CombinePath.hpp similarity index 100% rename from lib/appbase/util/CombinePath.hpp rename to src/util/CombinePath.hpp diff --git a/lib/appbase/util/DistanceBetweenPoints.hpp b/src/util/DistanceBetweenPoints.hpp similarity index 100% rename from lib/appbase/util/DistanceBetweenPoints.hpp rename to src/util/DistanceBetweenPoints.hpp diff --git a/lib/appbase/util/FunctionEventFilter.cpp b/src/util/FunctionEventFilter.cpp similarity index 86% rename from lib/appbase/util/FunctionEventFilter.cpp rename to src/util/FunctionEventFilter.cpp index 9b0ab94f..5071e6e9 100644 --- a/lib/appbase/util/FunctionEventFilter.cpp +++ b/src/util/FunctionEventFilter.cpp @@ -1,6 +1,6 @@ #include "FunctionEventFilter.hpp" -namespace AB_NAMESPACE { +namespace chatterino { FunctionEventFilter::FunctionEventFilter( QObject *parent, std::function function) @@ -14,4 +14,4 @@ bool FunctionEventFilter::eventFilter(QObject *watched, QEvent *event) return this->function_(watched, event); } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/util/FunctionEventFilter.hpp b/src/util/FunctionEventFilter.hpp similarity index 88% rename from lib/appbase/util/FunctionEventFilter.hpp rename to src/util/FunctionEventFilter.hpp index 99cb7d9c..bde2b51f 100644 --- a/lib/appbase/util/FunctionEventFilter.hpp +++ b/src/util/FunctionEventFilter.hpp @@ -4,7 +4,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { class FunctionEventFilter : public QObject { @@ -21,4 +21,4 @@ private: std::function function_; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/util/FuzzyConvert.cpp b/src/util/FuzzyConvert.cpp similarity index 100% rename from lib/appbase/util/FuzzyConvert.cpp rename to src/util/FuzzyConvert.cpp diff --git a/lib/appbase/util/FuzzyConvert.hpp b/src/util/FuzzyConvert.hpp similarity index 100% rename from lib/appbase/util/FuzzyConvert.hpp rename to src/util/FuzzyConvert.hpp diff --git a/lib/appbase/util/Helpers.cpp b/src/util/Helpers.cpp similarity index 93% rename from lib/appbase/util/Helpers.cpp rename to src/util/Helpers.cpp index 8658b2ce..c537f797 100644 --- a/lib/appbase/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -2,7 +2,7 @@ #include -namespace AB_NAMESPACE { +namespace chatterino { QString generateUuid() { @@ -35,4 +35,4 @@ QString shortenString(const QString &str, unsigned maxWidth) return shortened; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/util/Helpers.hpp b/src/util/Helpers.hpp similarity index 92% rename from lib/appbase/util/Helpers.hpp rename to src/util/Helpers.hpp index bc2e714e..cb37a182 100644 --- a/lib/appbase/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -3,7 +3,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { template auto fS(Args &&... args) @@ -20,7 +20,7 @@ QString formatRichNamedLink(const QString &url, const QString &name, QString shortenString(const QString &str, unsigned maxWidth = 50); -} // namespace AB_NAMESPACE +} // namespace chatterino namespace fmt { diff --git a/src/util/InitUpdateButton.cpp b/src/util/InitUpdateButton.cpp index 95264503..826c8feb 100644 --- a/src/util/InitUpdateButton.cpp +++ b/src/util/InitUpdateButton.cpp @@ -22,14 +22,12 @@ void initUpdateButton(Button &button, dialog->buttonClicked.connect([&button](auto buttonType) { switch (buttonType) { - case UpdateDialog::Dismiss: - { + case UpdateDialog::Dismiss: { button.hide(); } break; - case UpdateDialog::Install: - { - Updates::getInstance().installUpdates(); + case UpdateDialog::Install: { + Updates::instance().installUpdates(); } break; } @@ -41,18 +39,18 @@ void initUpdateButton(Button &button, // update image when state changes auto updateChange = [&button](auto) { - button.setVisible(Updates::getInstance().shouldShowUpdateButton()); + button.setVisible(Updates::instance().shouldShowUpdateButton()); - auto imageUrl = Updates::getInstance().isError() + auto imageUrl = Updates::instance().isError() ? ":/buttons/updateError.png" : ":/buttons/update.png"; button.setPixmap(QPixmap(imageUrl)); }; - updateChange(Updates::getInstance().getStatus()); + updateChange(Updates::instance().getStatus()); signalHolder.managedConnect( - Updates::getInstance().statusUpdated, + Updates::instance().statusUpdated, [updateChange](auto status) { updateChange(status); }); } diff --git a/src/util/IrcHelpers.hpp b/src/util/IrcHelpers.hpp index 1c605c94..9fb8ce2b 100644 --- a/src/util/IrcHelpers.hpp +++ b/src/util/IrcHelpers.hpp @@ -19,38 +19,32 @@ inline QString parseTagString(const QString &input) switch (c.cell()) { - case 'n': - { + case 'n': { output.replace(i, 2, '\n'); } break; - case 'r': - { + case 'r': { output.replace(i, 2, '\r'); } break; - case 's': - { + case 's': { output.replace(i, 2, ' '); } break; - case '\\': - { + case '\\': { output.replace(i, 2, '\\'); } break; - case ':': - { + case ':': { output.replace(i, 2, ';'); } break; - default: - { + default: { output.remove(i, 1); } break; diff --git a/lib/appbase/util/LayoutHelper.hpp b/src/util/LayoutHelper.hpp similarity index 100% rename from lib/appbase/util/LayoutHelper.hpp rename to src/util/LayoutHelper.hpp diff --git a/lib/appbase/util/PostToThread.hpp b/src/util/PostToThread.hpp similarity index 95% rename from lib/appbase/util/PostToThread.hpp rename to src/util/PostToThread.hpp index 5ea30c1a..ab0fbe1c 100644 --- a/lib/appbase/util/PostToThread.hpp +++ b/src/util/PostToThread.hpp @@ -10,7 +10,7 @@ #define async_exec(a) \ QThreadPool::globalInstance()->start(new LambdaRunnable(a)); -namespace AB_NAMESPACE { +namespace chatterino { class LambdaRunnable : public QRunnable { @@ -56,4 +56,4 @@ static void postToThread(F &&fun, QObject *obj = qApp) QCoreApplication::postEvent(obj, new Event(std::forward(fun))); } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/util/QObjectRef.hpp b/src/util/QObjectRef.hpp index 4a15545d..bd469d66 100644 --- a/src/util/QObjectRef.hpp +++ b/src/util/QObjectRef.hpp @@ -61,10 +61,10 @@ private: // new if (other) { - this->conn_ = - QObject::connect(other, &QObject::destroyed, qApp, - [this](QObject *) { this->set(nullptr); }, - Qt::DirectConnection); + this->conn_ = QObject::connect( + other, &QObject::destroyed, qApp, + [this](QObject *) { this->set(nullptr); }, + Qt::DirectConnection); } this->t_ = other; diff --git a/src/util/QStringHash.hpp b/src/util/QStringHash.hpp index 6e2d62b8..1152e76e 100644 --- a/src/util/QStringHash.hpp +++ b/src/util/QStringHash.hpp @@ -5,6 +5,7 @@ namespace std { +#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) template <> struct hash { std::size_t operator()(const QString &s) const @@ -12,5 +13,6 @@ struct hash { return qHash(s); } }; +#endif } // namespace std diff --git a/lib/appbase/util/RapidJsonSerializeQString.hpp b/src/util/RapidJsonSerializeQString.hpp similarity index 100% rename from lib/appbase/util/RapidJsonSerializeQString.hpp rename to src/util/RapidJsonSerializeQString.hpp diff --git a/src/util/SampleLinks.hpp b/src/util/SampleLinks.hpp new file mode 100644 index 00000000..4ddc9b41 --- /dev/null +++ b/src/util/SampleLinks.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include + +namespace chatterino { + +QStringList getValidLinks() +{ + return { + R"(http://github.com/)", + R"(https://github.com/)", + R"(http://username@github.com/)", + R"(https://username@github.com/)", + R"(http://pajlada.github.io)", + R"(https://pajlada.github.io)", + R"(http://pajlada.github.io/)", + R"(https://pajlada.github.io/)", + R"(http://github.com/some/random/path)", + R"(https://github.com/some/random/path)", + R"(http://github.com?query=value)", + R"(https://github.com?query=value)", + R"(http://github.com?query=value&abc=123)", + R"(https://github.com?query=value&abc=123)", + R"(http://github.com/?query=value&abc=123&yhf=abc_def)", + R"(http://github.com/?query=value&abc=123&yhf)", + R"(http://github.com?query=value&abc=)", + R"(http://github.com?query=value&abc=)", + R"(http://github.com/#block)", + R"(https://github.com/#anchor)", + R"(http://github.com/path/?qs=true#block)", + R"(https://github.com/path/?qs=true#anchor)", + R"(github.com/)", + R"(username@github.com/)", + R"(pajlada.github.io)", + R"(pajlada.github.io/)", + R"(github.com/some/random/path)", + R"(github.com?query=value)", + R"(github.com?query=value&abc=123)", + R"(github.com/?query=value&abc=123&yhf=abc_def)", + R"(github.com/?query=value&abc=123&yhf)", + R"(github.com?query=value&abc=)", + R"(github.com?query=value&abc=)", + R"(github.com/#block)", + R"(github.com/path/?qs=true#block)", + R"(HTTP://GITHUB.COM/)", + R"(HTTPS://GITHUB.COM/)", + R"(HTTP://USERNAME@GITHUB.COM/)", + R"(HTTPS://USERNAME@GITHUB.COM/)", + R"(HTTP://PAJLADA.GITHUB.IO)", + R"(HTTPS://PAJLADA.GITHUB.IO)", + R"(HTTP://PAJLADA.GITHUB.IO/)", + R"(HTTPS://PAJLADA.GITHUB.IO/)", + R"(HTTP://GITHUB.COM/SOME/RANDOM/PATH)", + R"(HTTPS://GITHUB.COM/SOME/RANDOM/PATH)", + R"(HTTP://GITHUB.COM?QUERY=VALUE)", + R"(HTTPS://GITHUB.COM?QUERY=VALUE)", + R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=123)", + R"(HTTPS://GITHUB.COM?QUERY=VALUE&ABC=123)", + R"(HTTP://GITHUB.COM/?QUERY=VALUE&ABC=123&YHF=ABC_DEF)", + R"(HTTP://GITHUB.COM/?QUERY=VALUE&ABC=123&YHF)", + R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=)", + R"(HTTP://GITHUB.COM?QUERY=VALUE&ABC=)", + R"(HTTP://GITHUB.COM/#BLOCK)", + R"(HTTPS://GITHUB.COM/#ANCHOR)", + R"(HTTP://GITHUB.COM/PATH/?QS=TRUE#BLOCK)", + R"(HTTPS://GITHUB.COM/PATH/?QS=TRUE#ANCHOR)", + R"(GITHUB.COM/)", + R"(USERNAME@GITHUB.COM/)", + R"(PAJLADA.GITHUB.IO)", + R"(PAJLADA.GITHUB.IO/)", + R"(GITHUB.COM/SOME/RANDOM/PATH)", + R"(GITHUB.COM?QUERY=VALUE)", + R"(GITHUB.COM?QUERY=VALUE&ABC=123)", + R"(GITHUB.COM/?QUERY=VALUE&ABC=123&YHF=ABC_DEF)", + R"(GITHUB.COM/?QUERY=VALUE&ABC=123&YHF)", + R"(GITHUB.COM?QUERY=VALUE&ABC=)", + R"(GITHUB.COM?QUERY=VALUE&ABC=)", + R"(GITHUB.COM/#BLOCK)", + R"(GITHUB.COM/PATH/?QS=TRUE#BLOCK)", + R"(http://foo.com/blah_blah)", + R"(http://foo.com/blah_blah/)", + R"(http://foo.com/blah_blah_(wikipedia))", + R"(http://foo.com/blah_blah_(wikipedia)_(again))", + R"(http://www.example.com/wpstyle/?p=364)", + R"(https://www.example.com/foo/?bar=baz&inga=42&quux)", + R"(http://✪df.ws/123)", + R"(http://userid@example.com)", + R"(http://userid@example.com/)", + R"(http://userid@example.com:8080)", + R"(http://userid@example.com:8080/)", + R"(http://142.42.1.1/)", + R"(http://142.42.1.1:8080/)", + R"(http://➡.ws/䨹)", + R"(http://⌘.ws)", + R"(http://⌘.ws/)", + R"(http://foo.com/blah_(wikipedia)#cite-1)", + R"(http://foo.com/blah_(wikipedia)_blah#cite-1)", + R"(http://foo.com/unicode_(✪)_in_parens)", + R"(http://foo.com/(something)?after=parens)", + R"(http://☺.damowmow.com/)", + R"(http://code.google.com/events/#&product=browser)", + R"(http://j.mp)", + R"(ftp://foo.bar/baz)", + R"(http://foo.bar/?q=Test%20URL-encoded%20stuff)", + R"(http://مثال.إختبار)", + R"(http://例子.测试)", + R"(http://उदाहरण.परीक्षा)", + R"(http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com)", + R"(http://1337.net)", + R"(http://a.b-c.de)", + R"(http://223.255.255.254)", + }; +} + +QStringList getValidButIgnoredLinks() +{ + return { + R"(http://username:password@github.com/)", + R"(https://username:password@github.com/)", + R"(http://userid:password@example.com)", + R"(http://userid:password@example.com/)", + R"(http://userid:password@example.com:8080)", + R"(http://userid:password@example.com:8080/)", + }; +} + +QStringList getInvalidLinks() +{ + return { + R"(1.40)", + R"(test..)", + R"(test.)", + R"(http://)", + R"(http://.)", + R"(http://..)", + R"(http://../)", + R"(http://?)", + R"(http://??)", + R"(http://??/)", + R"(http://#)", + R"(http://##)", + R"(http://##/)", + R"(http://foo.bar?q=Spaces should be encoded)", + R"(//)", + R"(//a)", + R"(///a)", + R"(///)", + R"(http:///a)", + R"(foo.com)", + R"(rdar://1234)", + R"(h://test)", + R"(http:// shouldfail.com)", + R"(:// should fail)", + R"(http://foo.bar/foo(bar)baz quux)", + R"(ftps://foo.bar/)", + R"(http://-error-.invalid/)", + R"(http://a.b--c.de/)", + R"(http://-a.b.co)", + R"(http://a.b-.co)", + R"(http://0.0.0.0)", + R"(http://10.1.1.0)", + R"(http://10.1.1.255)", + R"(http://224.1.1.1)", + R"(http://1.1.1.1.1)", + R"(http://123.123.123)", + R"(http://3628126748)", + R"(http://.www.foo.bar/)", + R"(http://www.foo.bar./)", + R"(http://.www.foo.bar./)", + R"(http://10.1.1.1)", + }; +} + +} // namespace chatterino diff --git a/lib/appbase/util/Shortcut.hpp b/src/util/Shortcut.hpp similarity index 91% rename from lib/appbase/util/Shortcut.hpp rename to src/util/Shortcut.hpp index faddab9b..1b20d1c1 100644 --- a/lib/appbase/util/Shortcut.hpp +++ b/src/util/Shortcut.hpp @@ -3,7 +3,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { template inline void createShortcut(WidgetType *w, const char *key, Func func) @@ -21,4 +21,4 @@ inline void createWindowShortcut(WidgetType *w, const char *key, Func func) QObject::connect(s, &QShortcut::activated, w, func); } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index 4c3634c2..9c70adb2 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -199,7 +199,7 @@ void openStreamlinkForChannel(const QString &channel) { QString channelURL = "twitch.tv/" + channel; - QString preferredQuality = getSettings()->preferredQuality; + QString preferredQuality = getSettings()->preferredQuality.getValue(); preferredQuality = preferredQuality.toLower(); if (preferredQuality == "choose") diff --git a/lib/appbase/util/WindowsHelper.cpp b/src/util/WindowsHelper.cpp similarity index 97% rename from lib/appbase/util/WindowsHelper.cpp rename to src/util/WindowsHelper.cpp index b2649b85..d2b9a4ef 100644 --- a/lib/appbase/util/WindowsHelper.cpp +++ b/src/util/WindowsHelper.cpp @@ -4,7 +4,7 @@ #ifdef USEWINSDK -namespace AB_NAMESPACE { +namespace chatterino { typedef enum MONITOR_DPI_TYPE { MDT_EFFECTIVE_DPI = 0, @@ -81,6 +81,6 @@ void setRegisteredForStartup(bool isRegistered) } } -} // namespace AB_NAMESPACE +} // namespace chatterino #endif diff --git a/lib/appbase/util/WindowsHelper.hpp b/src/util/WindowsHelper.hpp similarity index 82% rename from lib/appbase/util/WindowsHelper.hpp rename to src/util/WindowsHelper.hpp index e32f4b26..b40a8270 100644 --- a/lib/appbase/util/WindowsHelper.hpp +++ b/src/util/WindowsHelper.hpp @@ -5,7 +5,7 @@ # include # include -namespace AB_NAMESPACE { +namespace chatterino { boost::optional getWindowDpi(HWND hwnd); void flushClipboard(); @@ -13,6 +13,6 @@ void flushClipboard(); bool isRegisteredForStartup(); void setRegisteredForStartup(bool isRegistered); -} // namespace AB_NAMESPACE +} // namespace chatterino #endif diff --git a/src/widgets/AttachedWindow.cpp b/src/widgets/AttachedWindow.cpp index 155741df..04c0dde0 100644 --- a/src/widgets/AttachedWindow.cpp +++ b/src/widgets/AttachedWindow.cpp @@ -1,6 +1,7 @@ #include "AttachedWindow.hpp" #include "Application.hpp" +#include "singletons/Settings.hpp" #include "util/DebugCount.hpp" #include "widgets/splits/Split.hpp" @@ -180,13 +181,19 @@ void AttachedWindow::attachToHwnd(void *_attachedPtr) QString qfilename = QString::fromWCharArray(filename.get(), int(filenameLength)); - if (!qfilename.endsWith("chrome.exe") && - !qfilename.endsWith("firefox.exe")) + if (!getSettings()->attachExtensionToAnyProcess) { - qDebug() << "NM Illegal caller" << qfilename; - this->timer_.stop(); - this->deleteLater(); - return; + // We don't attach to non-browser processes by default. + if (!qfilename.endsWith("chrome.exe") && + !qfilename.endsWith("firefox.exe") && + !qfilename.endsWith("vivaldi.exe") && + !qfilename.endsWith("opera.exe")) + { + qDebug() << "NM Illegal caller" << qfilename; + this->timer_.stop(); + this->deleteLater(); + return; + } } this->validProcessName_ = true; } diff --git a/src/widgets/BasePopup.cpp b/src/widgets/BasePopup.cpp new file mode 100644 index 00000000..f8c5c5e6 --- /dev/null +++ b/src/widgets/BasePopup.cpp @@ -0,0 +1,21 @@ +#include "widgets/BasePopup.hpp" + +namespace chatterino { + +BasePopup::BasePopup(FlagsEnum _flags, QWidget *parent) + : BaseWindow(std::move(_flags), parent) +{ +} + +void BasePopup::keyPressEvent(QKeyEvent *e) +{ + if (e->key() == Qt::Key_Escape) + { + this->close(); + return; + } + + BaseWindow::keyPressEvent(e); +} + +} // namespace chatterino diff --git a/src/widgets/BasePopup.hpp b/src/widgets/BasePopup.hpp new file mode 100644 index 00000000..6d7ab7d6 --- /dev/null +++ b/src/widgets/BasePopup.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "common/FlagsEnum.hpp" +#include "widgets/BaseWindow.hpp" + +namespace chatterino { + +class BasePopup : public BaseWindow +{ +public: + explicit BasePopup(FlagsEnum flags_ = None, + QWidget *parent = nullptr); + + virtual ~BasePopup() = default; + +protected: + void keyPressEvent(QKeyEvent *e) override; +}; + +} // namespace chatterino diff --git a/lib/appbase/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp similarity index 98% rename from lib/appbase/widgets/BaseWidget.cpp rename to src/widgets/BaseWidget.cpp index 0a80d91e..c4fb5304 100644 --- a/lib/appbase/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -11,7 +11,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) @@ -161,4 +161,4 @@ void BaseWidget::themeChangedEvent() // Do any color scheme updates here } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/BaseWidget.hpp b/src/widgets/BaseWidget.hpp similarity index 96% rename from lib/appbase/widgets/BaseWidget.hpp rename to src/widgets/BaseWidget.hpp index 6b390857..15f3884c 100644 --- a/lib/appbase/widgets/BaseWidget.hpp +++ b/src/widgets/BaseWidget.hpp @@ -5,7 +5,7 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { class Theme; class BaseWindow; @@ -56,4 +56,4 @@ private: friend class BaseWindow; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp similarity index 96% rename from lib/appbase/widgets/BaseWindow.cpp rename to src/widgets/BaseWindow.cpp index 95e95dfd..7e80818c 100644 --- a/lib/appbase/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -42,7 +42,7 @@ #include "widgets/helper/TitlebarButton.hpp" -namespace AB_NAMESPACE { +namespace chatterino { BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) : BaseWidget(parent, @@ -81,6 +81,8 @@ BaseWindow::BaseWindow(FlagsEnum _flags, QWidget *parent) QObject::connect(&this->useNextBounds_, &QTimer::timeout, this, [this]() { this->currentBounds_ = this->nextBounds_; }); #endif + + this->themeChangedEvent(); } void BaseWindow::setInitialBounds(const QRect &bounds) @@ -338,20 +340,17 @@ void BaseWindow::onFocusLost() { switch (this->getActionOnFocusLoss()) { - case Delete: - { + case Delete: { this->deleteLater(); } break; - case Close: - { + case Close: { this->close(); } break; - case Hide: - { + case Hide: { this->hide(); } break; @@ -462,7 +461,7 @@ void BaseWindow::changeEvent(QEvent *) { if (this->isVisible()) { - TooltipWidget::getInstance()->hide(); + TooltipWidget::instance()->hide(); } #ifdef USEWINSDK @@ -492,7 +491,7 @@ void BaseWindow::changeEvent(QEvent *) void BaseWindow::leaveEvent(QEvent *) { - TooltipWidget::getInstance()->hide(); + TooltipWidget::instance()->hide(); } void BaseWindow::moveTo(QWidget *parent, QPoint point, bool offset) @@ -516,6 +515,25 @@ void BaseWindow::resizeEvent(QResizeEvent *) //this->moveIntoDesktopRect(this); +#ifdef USEWINSDK + if (this->hasCustomWindowFrame() && !this->isResizeFixing_) + { + this->isResizeFixing_ = true; + QTimer::singleShot(50, this, [this] { + 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); + QTimer::singleShot(10, this, + [this] { this->isResizeFixing_ = false; }); + }); + } +#endif + this->calcButtonsSizes(); } @@ -536,11 +554,9 @@ void BaseWindow::closeEvent(QCloseEvent *) void BaseWindow::showEvent(QShowEvent *) { + this->moveIntoDesktopRect(this); if (this->frameless_) { - this->moveIntoDesktopRect(this); - qDebug() << "show"; - QTimer::singleShot(30, this, [this] { this->moveIntoDesktopRect(this); }); } @@ -997,4 +1013,4 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) #endif } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp similarity index 97% rename from lib/appbase/widgets/BaseWindow.hpp rename to src/widgets/BaseWindow.hpp index 683bd870..c9fbf63b 100644 --- a/lib/appbase/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -10,7 +10,7 @@ class QHBoxLayout; struct tagMSG; typedef struct tagMSG MSG; -namespace AB_NAMESPACE { +namespace chatterino { class Button; class EffectLabel; @@ -109,6 +109,7 @@ private: bool shown_ = false; FlagsEnum flags_; float nativeScale_ = 1; + bool isResizeFixing_ = false; struct { QLayout *windowLayout = nullptr; @@ -135,4 +136,4 @@ private: friend class BaseWidget; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/Label.cpp b/src/widgets/Label.cpp similarity index 89% rename from lib/appbase/widgets/Label.cpp rename to src/widgets/Label.cpp index 189b37a1..330ab852 100644 --- a/lib/appbase/widgets/Label.cpp +++ b/src/widgets/Label.cpp @@ -2,7 +2,7 @@ #include -namespace AB_NAMESPACE { +namespace chatterino { Label::Label(QString text, FontStyle style) : Label(nullptr, text, style) @@ -83,12 +83,20 @@ QSize Label::minimumSizeHint() const void Label::paintEvent(QPaintEvent *) { QPainter painter(this); + + qreal deviceDpi = +#ifdef Q_OS_WIN + this->devicePixelRatioF(); +#else + 1.0; +#endif + QFontMetrics metrics = getFonts()->getFontMetrics( this->getFontStyle(), - this->scale() * 96.f / this->logicalDpiX() * this->devicePixelRatioF()); + this->scale() * 96.f / this->logicalDpiX() * deviceDpi); painter.setFont(getFonts()->getFont( - this->getFontStyle(), this->scale() * 96.f / this->logicalDpiX() * - this->devicePixelRatioF())); + this->getFontStyle(), + this->scale() * 96.f / this->logicalDpiX() * deviceDpi)); int offset = this->getOffset(); @@ -129,4 +137,4 @@ int Label::getOffset() return this->hasOffset_ ? int(8 * this->scale()) : 0; } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/Label.hpp b/src/widgets/Label.hpp similarity index 95% rename from lib/appbase/widgets/Label.hpp rename to src/widgets/Label.hpp index 655971a6..0cf397a7 100644 --- a/lib/appbase/widgets/Label.hpp +++ b/src/widgets/Label.hpp @@ -5,7 +5,7 @@ #include -namespace AB_NAMESPACE { +namespace chatterino { class Label : public BaseWidget { @@ -47,4 +47,4 @@ private: pajlada::Signals::SignalHolder connections_; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 4c7efeed..30253e48 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -297,15 +297,13 @@ void Scrollbar::paintEvent(QPaintEvent *) switch (highlight.getStyle()) { - case ScrollbarHighlight::Default: - { + case ScrollbarHighlight::Default: { painter.fillRect(w / 8 * 3, int(y), w / 4, highlightHeight, color); } break; - case ScrollbarHighlight::Line: - { + case ScrollbarHighlight::Line: { painter.fillRect(0, int(y), w, 1, color); } break; diff --git a/lib/appbase/widgets/TooltipWidget.cpp b/src/widgets/TooltipWidget.cpp similarity index 96% rename from lib/appbase/widgets/TooltipWidget.cpp rename to src/widgets/TooltipWidget.cpp index b3cad0d1..d0898b45 100644 --- a/lib/appbase/widgets/TooltipWidget.cpp +++ b/src/widgets/TooltipWidget.cpp @@ -12,9 +12,9 @@ # include #endif -namespace AB_NAMESPACE { +namespace chatterino { -TooltipWidget *TooltipWidget::getInstance() +TooltipWidget *TooltipWidget::instance() { static TooltipWidget *tooltipWidget = new TooltipWidget(); return tooltipWidget; @@ -119,4 +119,4 @@ void TooltipWidget::leaveEvent(QEvent *) // clear parents event } -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/lib/appbase/widgets/TooltipWidget.hpp b/src/widgets/TooltipWidget.hpp similarity index 89% rename from lib/appbase/widgets/TooltipWidget.hpp rename to src/widgets/TooltipWidget.hpp index a9963ad3..c693f1df 100644 --- a/lib/appbase/widgets/TooltipWidget.hpp +++ b/src/widgets/TooltipWidget.hpp @@ -6,14 +6,14 @@ #include #include -namespace AB_NAMESPACE { +namespace chatterino { class TooltipWidget : public BaseWindow { Q_OBJECT public: - static TooltipWidget *getInstance(); + static TooltipWidget *instance(); TooltipWidget(BaseWidget *parent = nullptr); ~TooltipWidget() override; @@ -42,4 +42,4 @@ private: pajlada::Signals::Connection fontChangedConnection_; }; -} // namespace AB_NAMESPACE +} // namespace chatterino diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 79b63767..2824a8b3 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -24,19 +24,19 @@ #include "widgets/splits/Split.hpp" #include "widgets/splits/SplitContainer.hpp" -#ifdef QT_DEBUG +#ifdef C_DEBUG # include "util/SampleCheerMessages.hpp" +# include "util/SampleLinks.hpp" #endif #include #include #include +#include #include #include -#include - -#include #include +#include namespace chatterino { @@ -86,8 +86,7 @@ bool Window::event(QEvent *event) case QEvent::WindowActivate: break; - case QEvent::WindowDeactivate: - { + case QEvent::WindowDeactivate: { auto page = this->notebook_->getOrAddSelectedPage(); if (page != nullptr) @@ -200,10 +199,12 @@ void Window::addCustomTitlebarButtons() void Window::addDebugStuff() { -#ifdef QT_DEBUG - std::vector cheerMessages, subMessages, miscMessages; +#ifdef C_DEBUG + std::vector cheerMessages, subMessages, miscMessages, linkMessages; cheerMessages = getSampleCheerMessage(); + auto validLinks = getValidLinks(); + auto invalidLinks = getInvalidLinks(); // clang-format off subMessages.emplace_back(R"(@badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=ronni;emotes=;id=db25007f-7a18-43eb-9379-80131e44d633;login=ronni;mod=0;msg-id=resub;msg-param-months=6;msg-param-sub-plan=Prime;msg-param-sub-plan-name=Prime;room-id=1337;subscriber=1;system-msg=ronni\shas\ssubscribed\sfor\s6\smonths!;tmi-sent-ts=1507246572675;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #pajlada :Great stream -- keep it up!)"); @@ -232,6 +233,12 @@ void Window::addDebugStuff() // display name renders strangely miscMessages.emplace_back(R"(@badges=;color=#00AD2B;display-name=Iamme420\s;emotes=;id=d47a1e4b-a3c6-4b9e-9bf1-51b8f3dbc76e;mod=0;room-id=11148817;subscriber=0;tmi-sent-ts=1529670347537;turbo=0;user-id=56422869;user-type= :iamme420!iamme420@iamme420.tmi.twitch.tv PRIVMSG #pajlada :offline chat gachiBASS)"); + miscMessages.emplace_back(R"(@badge-info=founder/47;badges=moderator/1,founder/0,premium/1;color=#00FF80;display-name=gempir;emotes=;flags=;id=d4514490-202e-43cb-b429-ef01a9d9c2fe;mod=1;room-id=11148817;subscriber=0;tmi-sent-ts=1575198233854;turbo=0;user-id=77829817;user-type=mod :gempir!gempir@gempir.tmi.twitch.tv PRIVMSG #pajlada :offline chat gachiBASS)"); + + // various link tests + linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should pass: )" + getValidLinks().join(' ')); + linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should NOT pass: )" + getInvalidLinks().join(' ')); + linkMessages.emplace_back(R"(@badge-info=subscriber/48;badges=broadcaster/1,subscriber/36,partner/1;color=#CC44FF;display-name=pajlada;emotes=;flags=;id=3c23cf3c-0864-4699-a76b-089350141147;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1577628844607;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada : Links that should technically pass but we choose not to parse them: )" + getValidButIgnoredLinks().join(' ')); // clang-format on createWindowShortcut(this, "F6", [=] { @@ -249,6 +256,14 @@ void Window::addDebugStuff() getApp()->twitch.server->addFakeMessage(msg); }); + createWindowShortcut(this, "F8", [=] { + const auto &messages = linkMessages; + static int index = 0; + auto app = getApp(); + const auto &msg = messages[index++ % messages.size()]; + app->twitch.server->addFakeMessage(msg); + }); + createWindowShortcut(this, "F9", [=] { auto *dialog = new WelcomeDialog(); dialog->setAttribute(Qt::WA_DeleteOnClose); @@ -369,7 +384,7 @@ void Window::onAccountSelected() auto user = getApp()->accounts->twitch.getCurrent(); // update title - this->setWindowTitle(Version::getInstance().getFullVersion()); + this->setWindowTitle(Version::instance().fullVersion()); // update user if (user->isAnon()) diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 11c3ed88..6aebd214 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -101,7 +101,7 @@ namespace { } // namespace EmotePopup::EmotePopup(QWidget *parent) - : BaseWindow(BaseWindow::EnableCustomFrame, parent) + : BasePopup(BaseWindow::EnableCustomFrame, parent) { auto layout = new QVBoxLayout(this); this->getLayoutContainer()->setLayout(layout); diff --git a/src/widgets/dialogs/EmotePopup.hpp b/src/widgets/dialogs/EmotePopup.hpp index 4997e578..7e24faf9 100644 --- a/src/widgets/dialogs/EmotePopup.hpp +++ b/src/widgets/dialogs/EmotePopup.hpp @@ -1,6 +1,6 @@ #pragma once -#include "widgets/BaseWindow.hpp" +#include "widgets/BasePopup.hpp" #include @@ -11,7 +11,7 @@ class ChannelView; class Channel; using ChannelPtr = std::shared_ptr; -class EmotePopup : public BaseWindow +class EmotePopup : public BasePopup { public: EmotePopup(QWidget *parent = nullptr); diff --git a/src/widgets/dialogs/LastRunCrashDialog.cpp b/src/widgets/dialogs/LastRunCrashDialog.cpp index e70b2bb5..1233116f 100644 --- a/src/widgets/dialogs/LastRunCrashDialog.cpp +++ b/src/widgets/dialogs/LastRunCrashDialog.cpp @@ -31,7 +31,7 @@ LastRunCrashDialog::LastRunCrashDialog() // QDialogButtonBox::NoRole); installUpdateButton->setEnabled(false); // QObject::connect(installUpdateButton, &QPushButton::clicked, [this, // update]() mutable { - // auto &updateManager = UpdateManager::getInstance(); + // auto &updateManager = UpdateManager::instance(); // updateManager.installUpdates(); // this->setEnabled(false); @@ -45,7 +45,7 @@ LastRunCrashDialog::LastRunCrashDialog() // Updates // auto updateUpdateLabel = [update]() mutable { - // auto &updateManager = UpdateManager::getInstance(); + // auto &updateManager = UpdateManager::instance(); // switch (updateManager.getStatus()) { // case UpdateManager::None: { diff --git a/src/widgets/dialogs/NotificationPopup.cpp b/src/widgets/dialogs/NotificationPopup.cpp index e8ed2d82..539da739 100644 --- a/src/widgets/dialogs/NotificationPopup.cpp +++ b/src/widgets/dialogs/NotificationPopup.cpp @@ -35,8 +35,7 @@ void NotificationPopup::updatePosition() switch (location) { - case BottomRight: - { + case BottomRight: { this->move(rect.right() - this->width(), rect.bottom() - this->height()); } diff --git a/src/widgets/dialogs/SelectChannelDialog.cpp b/src/widgets/dialogs/SelectChannelDialog.cpp index e95c9034..25d61ff0 100644 --- a/src/widgets/dialogs/SelectChannelDialog.cpp +++ b/src/widgets/dialogs/SelectChannelDialog.cpp @@ -133,8 +133,8 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) auto outerBox = obj.setLayoutType(); { - auto view = this->ui_.irc.servers = new EditableModelView( - Irc::getInstance().newConnectionModel(this)); + auto view = this->ui_.irc.servers = + new EditableModelView(Irc::instance().newConnectionModel(this)); view->setTitles({"host", "port", "ssl", "user", "nick", "real", "password", "login command"}); @@ -147,12 +147,12 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) view->addButtonPressed.connect([] { auto unique = IrcServerData{}; - unique.id = Irc::getInstance().uniqueId(); + unique.id = Irc::instance().uniqueId(); auto editor = new IrcConnectionEditor(unique); if (editor->exec() == QDialog::Accepted) { - Irc::getInstance().connections.appendItem(editor->data()); + Irc::instance().connections.appendItem(editor->data()); } }); @@ -160,23 +160,21 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) view->getTableView(), &QTableView::doubleClicked, [](const QModelIndex &index) { auto editor = new IrcConnectionEditor( - Irc::getInstance() + Irc::instance() .connections.getVector()[size_t(index.row())]); if (editor->exec() == QDialog::Accepted) { auto data = editor->data(); - auto &&conns = - Irc::getInstance().connections.getVector(); + auto &&conns = Irc::instance().connections.getVector(); int i = 0; for (auto &&conn : conns) { if (conn.id == data.id) { - Irc::getInstance().connections.removeItem( + Irc::instance().connections.removeItem( i, Irc::noEraseCredentialCaller); - Irc::getInstance().connections.insertItem(data, - i); + Irc::instance().connections.insertItem(data, i); } i++; } @@ -190,6 +188,12 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) auto tab = notebook->addPage(obj.getElement()); tab->setCustomTitle("Irc (Beta)"); + + if (!getSettings()->enableExperimentalIrc) + { + tab->setEnable(false); + tab->setVisible(false); + } } layout->setStretchFactor(notebook.getElement(), 1); @@ -217,7 +221,12 @@ SelectChannelDialog::SelectChannelDialog(QWidget *parent) [=] { this->close(); }); // restore ui state - this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab); + // fourtf: enable when releasing irc + if (getSettings()->enableExperimentalIrc) + { + this->ui_.notebook->selectIndex(getSettings()->lastSelectChannelTab); + } + this->ui_.irc.servers->getTableView()->selectRow( getSettings()->lastSelectIrcConn); } @@ -247,33 +256,28 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) switch (_channel.getType()) { - case Channel::Type::Twitch: - { + case Channel::Type::Twitch: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.channel->setFocus(); this->ui_.twitch.channelName->setText(channel->getName()); } break; - case Channel::Type::TwitchWatching: - { + case Channel::Type::TwitchWatching: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.watching->setFocus(); } break; - case Channel::Type::TwitchMentions: - { + case Channel::Type::TwitchMentions: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.mentions->setFocus(); } break; - case Channel::Type::TwitchWhispers: - { + case Channel::Type::TwitchWhispers: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.whispers->setFocus(); } break; - case Channel::Type::Irc: - { + case Channel::Type::Irc: { this->ui_.notebook->selectIndex(TAB_IRC); this->ui_.irc.channel->setText(_channel.get()->getName()); @@ -283,7 +287,7 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) if (auto server = ircChannel->server()) { int i = 0; - for (auto &&conn : Irc::getInstance().connections) + for (auto &&conn : Irc::instance().connections) { if (conn.id == server->id()) { @@ -298,8 +302,7 @@ void SelectChannelDialog::setSelectedChannel(IndirectChannel _channel) this->ui_.irc.channel->setFocus(); } break; - default: - { + default: { this->ui_.notebook->selectIndex(TAB_TWITCH); this->ui_.twitch.channel->setFocus(); } @@ -319,8 +322,7 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const switch (this->ui_.notebook->getSelectedIndex()) { - case TAB_TWITCH: - { + case TAB_TWITCH: { if (this->ui_.twitch.channel->isChecked()) { return app->twitch.server->getOrAddChannel( @@ -340,18 +342,17 @@ IndirectChannel SelectChannelDialog::getSelectedChannel() const } } break; - case TAB_IRC: - { + case TAB_IRC: { int row = this->ui_.irc.servers->getTableView() ->selectionModel() ->currentIndex() .row(); - auto &&vector = Irc::getInstance().connections.getVector(); + auto &&vector = Irc::instance().connections.getVector(); if (row >= 0 && row < int(vector.size())) { - return Irc::getInstance().getOrAddChannel( + return Irc::instance().getOrAddChannel( vector[size_t(row)].id, this->ui_.irc.channel->text()); } else diff --git a/src/widgets/dialogs/SettingsDialog.cpp b/src/widgets/dialogs/SettingsDialog.cpp index d3edcb77..73ad5f75 100644 --- a/src/widgets/dialogs/SettingsDialog.cpp +++ b/src/widgets/dialogs/SettingsDialog.cpp @@ -285,6 +285,11 @@ void SettingsDialog::themeChangedEvent() this->setPalette(palette); } +void SettingsDialog::showEvent(QShowEvent *) +{ + this->ui_.search->setText(""); +} + ///// Widget creation helpers void SettingsDialog::onOkClicked() { diff --git a/src/widgets/dialogs/SettingsDialog.hpp b/src/widgets/dialogs/SettingsDialog.hpp index ff409fec..5a064b66 100644 --- a/src/widgets/dialogs/SettingsDialog.hpp +++ b/src/widgets/dialogs/SettingsDialog.hpp @@ -39,6 +39,7 @@ public: protected: virtual void scaleChangedEvent(float newDpi) override; virtual void themeChangedEvent() override; + virtual void showEvent(QShowEvent *) override; private: static SettingsDialog *handle; diff --git a/src/widgets/dialogs/UpdateDialog.cpp b/src/widgets/dialogs/UpdateDialog.cpp index bc84e667..4d96a38a 100644 --- a/src/widgets/dialogs/UpdateDialog.cpp +++ b/src/widgets/dialogs/UpdateDialog.cpp @@ -26,7 +26,7 @@ UpdateDialog::UpdateDialog() auto dismiss = buttons->addButton("Dismiss", QDialogButtonBox::RejectRole); QObject::connect(install, &QPushButton::clicked, this, [this] { - Updates::getInstance().installUpdates(); + Updates::instance().installUpdates(); this->close(); }); QObject::connect(dismiss, &QPushButton::clicked, this, [this] { @@ -34,9 +34,9 @@ UpdateDialog::UpdateDialog() this->close(); }); - this->updateStatusChanged(Updates::getInstance().getStatus()); + this->updateStatusChanged(Updates::instance().getStatus()); this->connections_.managedConnect( - Updates::getInstance().statusUpdated, + Updates::instance().statusUpdated, [this](auto status) { this->updateStatusChanged(status); }); this->setScaleIndependantHeight(150); @@ -48,38 +48,41 @@ void UpdateDialog::updateStatusChanged(Updates::Status status) switch (status) { - case Updates::UpdateAvailable: - { + case Updates::UpdateAvailable: { this->ui_.label->setText( - QString("An update (%1) is available.\n\nDo you want to " - "download and install it?") - .arg(Updates::getInstance().getOnlineVersion())); + (Updates::instance().isDowngrade() + ? QString( + "The version online (%1) seems to be lower than the " + "current (%2).\nEither a version was reverted or " + "you are running a newer build.\n\nDo you want to " + "download and install it?") + .arg(Updates::instance().getOnlineVersion(), + Updates::instance().getCurrentVersion()) + : QString("An update (%1) is available.\n\nDo you want to " + "download and install it?") + .arg(Updates::instance().getOnlineVersion()))); this->updateGeometry(); } break; - case Updates::SearchFailed: - { + case Updates::SearchFailed: { this->ui_.label->setText("Failed to load version information."); } break; - case Updates::Downloading: - { + case Updates::Downloading: { this->ui_.label->setText( "Downloading updates.\n\nChatterino will restart " "automatically when the download is done."); } break; - case Updates::DownloadFailed: - { + case Updates::DownloadFailed: { this->ui_.label->setText("Failed to download the update."); } break; - case Updates::WriteFileFailed: - { + case Updates::WriteFileFailed: { this->ui_.label->setText("Failed to save the update to disk."); } break; diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 40beaedf..b0fc0071 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -28,17 +28,23 @@ namespace chatterino { namespace { - void addCopyableLabel(LayoutCreator box, Label **assign) + Label *addCopyableLabel(LayoutCreator box) { - auto label = box.emplace