Merge branch 'master' into git_is_pepega

This commit is contained in:
Mm2PL 2020-01-01 21:06:29 +01:00 committed by GitHub
commit 20d8da8f2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
174 changed files with 2445 additions and 1112 deletions

42
.CI/CreateAppImage.sh Normal file → Executable file
View file

@ -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
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
./linuxdeployqt-continuous-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

13
.CI/CreateDMG.sh Executable file
View file

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

0
.CI/InstallQTStylePlugins.sh Normal file → Executable file
View file

View file

@ -10,17 +10,17 @@ assignees: ''
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**To Reproduce**
**To reproduce**
<!-- Steps to reproduce the behavior -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. Use the integrated uploader of the issue form to upload images, or copy an image to the clipboard and paste it in the input box -->
**Chatterino version**
<!-- Please copy the version information from the "About" page in the Settings, e.g. `Chatterino 2.1.4 (commit 35c7853c4, 16.09.2019)` -->
<!-- Copy the version information from the "About" page in the Settings, e.g. `Chatterino 2.1.4 (commit 35c7853c4, 16.09.2019)` -->
**Additional context**
<!-- Add any other context about the problem here. -->
**Operating system**
<!-- E.g. Windows 10 -->
**Additional information**
<!-- If applicable, add additional context. -->

207
.github/workflows/build.yml vendored Normal file
View file

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

20
.github/workflows/check-formatting.yml vendored Normal file
View file

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

2
.gitmodules vendored
View file

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

View file

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

View file

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

220
CONTRIBUTING.md Normal file
View file

@ -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<QRegularExpressionMatch> 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<int> myVec; // <- other types call constructors instead, so no need for {}
// std::vector<int> 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> &&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<Object> 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<Channel>&` (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<Derived*>(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<int &>(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<float*>(&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<Type, DeleteLater>` 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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +0,0 @@
# include appbase
include($$PWD/appbase/main.pro)
INCLUDEPATH += $$PWD/appbase

View file

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

View file

@ -1,36 +0,0 @@
#pragma once
#include <QHash>
#include <QString>
#include <functional>
#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<chatterino::name> { \
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);

View file

@ -1,31 +0,0 @@
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QStandardPaths>
#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();
}

View file

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

View file

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

View file

@ -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)
!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/
}

@ -1 +1 @@
Subproject commit a31ffb037eadac65dba73ad2b2da6dafe31e3bf7
Subproject commit f3e7f97914d9bf1166d349a83d93a2b4f4743c39

View file

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

View file

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

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 2019 Artem Polishchuk <ego.cordatus@gmail.com> -->
<component type="desktop-application">
<id>com.chatterino.chatterino.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<content_rating type="oars-1.0">
<content_attribute id="social-chat">intense</content_attribute>
</content_rating>
<name>Chatterino</name>
<summary>
Chat client for twitch.tv
</summary>
<description>
<p>
Chatterino 2 is the second installment of the Twitch chat client series
"Chatterino".
</p>
</description>
<screenshots>
<screenshot type="default">
<image>https://chatterino.com/img/screenshot-3.png</image>
</screenshot>
</screenshots>
<keywords>
<keyword>chat</keyword>
<keyword>twitch</keyword>
</keywords>
<url type="homepage">https://chatterino.com/</url>
<url type="bugtracker">https://github.com/Chatterino/chatterino2/issues</url>
<url type="donation">https://streamelements.com/fourtf/tip</url>
<provides>
<binary>chatterino</binary>
</provides>
</component>

View file

@ -28,7 +28,7 @@
<file>buttons/unmod.png</file>
<file>buttons/update.png</file>
<file>buttons/updateError.png</file>
<file>chatterino.desktop</file>
<file>com.chatterino.chatterino.desktop</file>
<file>chatterino.icns</file>
<file>contributors.txt</file>
<file>emoji.json</file>

View file

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

View file

@ -1,5 +1,8 @@
#include "Application.hpp"
#include <atomic>
#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 <atomic>
#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<Resources2>())
, themes(&this->emplace<Theme>())
: themes(&this->emplace<Theme>())
, fonts(&this->emplace<Fonts>())
, emotes(&this->emplace<Emotes>())
, windows(&this->emplace<WindowManager>())
@ -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<SplitContainer *>(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();
}

View file

@ -1,11 +1,11 @@
#pragma once
#include "common/Singleton.hpp"
#include "singletons/NativeMessaging.hpp"
#include <QApplication>
#include <memory>
#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{};

View file

@ -4,7 +4,7 @@
#include "util/Clamp.hpp"
namespace AB_NAMESPACE {
namespace chatterino {
std::vector<std::weak_ptr<pajlada::Settings::SettingData>> _settings;
@ -125,4 +125,4 @@ AB_SETTINGS_CLASS *getABSettings()
return AB_SETTINGS_CLASS::instance;
}
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

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

View file

@ -1,6 +1,6 @@
#include "BaseTheme.hpp"
namespace AB_NAMESPACE {
namespace chatterino {
namespace {
double getMultiplierByTheme(const QString &themeName)
{
@ -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

View file

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

View file

@ -4,10 +4,16 @@
#include <QFile>
#include <QPalette>
#include <QStyleFactory>
#include <Qt>
#include <QtConcurrent>
#include <csignal>
#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 <QBreakpadHandler.h>
#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");

View file

@ -1,4 +1,5 @@
#include <QPixmap>
#include "common/Singleton.hpp"
namespace chatterino {

34
src/common/Args.cpp Normal file
View file

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

20
src/common/Args.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <QStringList>
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

View file

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

View file

@ -2,11 +2,11 @@
#include "BaseSettings.hpp"
namespace AB_NAMESPACE {
namespace chatterino {
void _registerSetting(std::weak_ptr<pajlada::Settings::SettingData> setting)
{
_actuallyRegisterSetting(setting);
}
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

@ -3,7 +3,7 @@
#include <QString>
#include <pajlada/settings.hpp>
namespace AB_NAMESPACE {
namespace chatterino {
void _registerSetting(std::weak_ptr<pajlada::Settings::SettingData> setting);
@ -85,4 +85,4 @@ public:
}
};
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

@ -1,16 +1,15 @@
#pragma once
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "common/ProviderId.hpp"
#include <QString>
#include <QWidget>
#include <boost/optional.hpp>
#include <boost/preprocessor.hpp>
#include <string>
#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 <typename T>
using QObjectPtr = std::unique_ptr<T, DeleteLater>;
} // namespace chatterino

View file

@ -143,7 +143,7 @@ namespace {
}
} // namespace
Credentials &Credentials::getInstance()
Credentials &Credentials::instance()
{
static Credentials creds;
return creds;
@ -166,7 +166,8 @@ 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,
QObject::connect(
job, &QKeychain::Job::finished, receiver,
[job, onLoaded = std::move(onLoaded)](auto) mutable {
onLoaded(job->textData());
},
@ -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();
}

View file

@ -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<void(const QString &)> &&onLoaded);

View file

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

View file

@ -1,5 +1,7 @@
#include "common/Env.hpp"
#include <QVariant>
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))
{
}

View file

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

View file

@ -1,67 +1,201 @@
#include "common/LinkParser.hpp"
#include <QFile>
#include <QMap>
#include <QRegularExpression>
#include <QString>
#include <QStringRef>
#include <QTextStream>
// 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<QString> &tlds()
{
static QSet<QString> tlds = [] {
QFile file(":/tlds.txt");
file.open(QFile::ReadOnly);
QTextStream stream(&file);
stream.setCodec("UTF-8");
int safetyMax = 20000;
QSet<QString> 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<int>(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

View file

@ -14,7 +14,8 @@ public:
QString getCaptured() const;
private:
QRegularExpressionMatch match_;
bool hasMatch_{false};
QString match_;
};
} // namespace chatterino

View file

@ -27,7 +27,7 @@ Modes::Modes()
}
}
const Modes &Modes::getInstance()
const Modes &Modes::instance()
{
static Modes instance;
return instance;

View file

@ -7,7 +7,7 @@ class Modes
public:
Modes();
static const Modes &getInstance();
static const Modes &instance();
bool isNightly{};
bool isPortable{};

View file

@ -2,7 +2,7 @@
#include <boost/noncopyable.hpp>
namespace AB_NAMESPACE {
namespace chatterino {
class Settings;
class Paths;
@ -23,4 +23,4 @@ public:
}
};
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

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

View file

@ -1,8 +1,9 @@
#pragma once
#include <QString>
#include <QtGlobal>
#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

View file

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

View file

@ -70,8 +70,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
{
switch (column)
{
case 0:
{
case 0: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -86,8 +85,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 1:
{
case 1: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -103,8 +101,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 2:
{
case 2: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -120,8 +117,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
}
}
break;
case 3:
{
case 3: {
// empty element
}
break;

View file

@ -119,8 +119,8 @@ struct Deserialize<chatterino::HighlightPhrase> {
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);
}
};

View file

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

View file

@ -4,7 +4,7 @@
#include <QThread>
#include <cassert>
namespace AB_NAMESPACE {
namespace chatterino {
static bool isGuiThread()
{
@ -18,4 +18,4 @@ static void assertInGuiThread()
#endif
}
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

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

View file

@ -5,7 +5,7 @@
#include <QElapsedTimer>
#include <boost/noncopyable.hpp>
namespace AB_NAMESPACE {
namespace chatterino {
class BenchmarkGuard : boost::noncopyable
{
@ -19,4 +19,4 @@ private:
QString name_;
};
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

@ -5,7 +5,7 @@
#include <QDebug>
#include <QTime>
namespace AB_NAMESPACE {
namespace chatterino {
template <typename... Args>
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>(args)...);
}
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

@ -1,13 +1,18 @@
#include <QApplication>
#include <QDebug>
#include <QMessageBox>
#include <QStringList>
#include <memory>
#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 <QApplication>
#include <QStringList>
#include <memory>
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;
}

View file

@ -1,5 +1,14 @@
#include "messages/Image.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <functional>
#include <thread>
#include "Application.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
@ -11,15 +20,6 @@
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTimer>
#include <functional>
#include <thread>
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<Url, std::weak_ptr<Image>> 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;

View file

@ -13,7 +13,7 @@
#include <pajlada/signals/signal.hpp>
#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<Image>;
/// This class is thread safe.
class Image : public std::enable_shared_from_this<Image>, 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<detail::Frames> frames_{};
QObject object_{};
};
} // namespace chatterino

View file

@ -32,8 +32,7 @@ std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
builder.message().flags.set(MessageFlag::PubSub);
builder
.emplace<ImageElement>(
Image::fromPixmap(getApp()->resources->twitch.automod),
.emplace<ImageElement>(Image::fromPixmap(getResources().twitch.automod),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("AutoMod");
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
@ -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);
}

View file

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

View file

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

View file

@ -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<MessageLayoutElement>(element));
// set current x
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()) /

View file

@ -1,5 +1,8 @@
#include "providers/bttv/BttvEmotes.hpp"
#include <QJsonArray>
#include <QThread>
#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 <QJsonArray>
#include <QThread>
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,
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 + "<br />Global BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
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,
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 + "<br />Channel BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
Url{emoteLinkFormat.arg(id.string)},
});
emotes[name] = cachedOrMake(std::move(emote), id);
}

View file

@ -1,13 +1,13 @@
#pragma once
#include "providers/irc/IrcConnection2.hpp"
#include <IrcMessage>
#include <functional>
#include <mutex>
#include <pajlada/signals/signal.hpp>
#include <pajlada/signals/signalholder.hpp>
#include <functional>
#include <mutex>
#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<IrcConnection, Deleter> writeConnection_ = nullptr;
std::unique_ptr<IrcConnection, Deleter> readConnection_ = nullptr;
QObjectPtr<IrcConnection> writeConnection_ = nullptr;
QObjectPtr<IrcConnection> readConnection_ = nullptr;
QTimer reconnectTimer_;
int falloffCounter_ = 1;

View file

@ -73,13 +73,13 @@ inline QString getCredentialName(const IrcServerData &data)
void IrcServerData::getPassword(
QObject *receiver, std::function<void(const QString &)> &&onLoaded) const
{
Credentials::getInstance().get("irc", getCredentialName(*this), receiver,
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;

View file

@ -36,7 +36,7 @@ class Irc
public:
Irc();
static Irc &getInstance();
static Irc &instance();
static inline void *const noEraseCredentialCaller =
reinterpret_cast<void *>(1);

View file

@ -66,6 +66,8 @@ void IrcServer::initializeConnection(IrcConnection *connection,
connection->setRealName(this->data_->real.isEmpty() ? this->data_->user
: this->data_->nick);
if (getSettings()->enableExperimentalIrc)
{
switch (this->data_->authType)
{
case IrcAuthType::Sasl:
@ -87,6 +89,7 @@ void IrcServer::initializeConnection(IrcConnection *connection,
default:
this->open(Both);
}
}
QObject::connect(
connection, &Communi::IrcConnection::socketError, this,
@ -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<Communi::IrcJoinMessage *>(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<Communi::IrcPartMessage *>(message);
if (auto it =

View file

@ -40,7 +40,7 @@ static QMap<QString, QString> 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());
}

View file

@ -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<MessagePtr> parseMessage(Channel *channel,

View file

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

View file

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

View file

@ -0,0 +1,31 @@
#include "providers/twitch/TwitchBadge.hpp"
#include <QSet>
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<QString> globalAuthority{"staff", "admin", "global_mod"};
const QSet<QString> channelAuthority{"moderator", "vip", "broadcaster"};
const QSet<QString> 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

View file

@ -0,0 +1,21 @@
#pragma once
#include "messages/MessageElement.hpp"
#include <QString>
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

View file

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

View file

@ -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<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, 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<Channel>(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<MessagePtr> 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

View file

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

View file

@ -18,6 +18,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
struct CheerEmote {
QColor color;
int minBits;
QRegularExpression regex;
EmotePtr animatedEmote;
EmotePtr staticEmote;

View file

@ -1,7 +1,11 @@
#include "TwitchIrcServer.hpp"
#include <IrcCommand>
#include <cassert>
#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 <IrcCommand>
#include <cassert>
// 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<Channel> 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<std::string> 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<Communi::IrcNoticeMessage *>(message));
}
}
void TwitchIrcServer::onReadConnected(IrcConnection *connection)

View file

@ -28,6 +28,10 @@
namespace {
const QSet<QString> zeroWidthEmotes{
"SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane",
};
QColor getRandomColor(const QVariant &userId)
{
static const std::vector<QColor> 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<QString, QString> parseBadgeInfos(const QVariantMap &tags)
{
std::map<QString, QString> 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<Badge> parseBadges(const QVariantMap &tags)
{
std::vector<Badge> 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<EmotePtr> 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;
// Try to fetch channel-specific bit badge
try
{
if (twitchChannel)
if (const auto &_badge = this->twitchChannel->twitchBadge(
"bits", cheerAmount))
{
this->emplace<BadgeElement>(
_badge.get(), MessageElementFlag::BadgeVanity)
->setTooltip(tooltip);
log("No channel/global variant found {}", badge.key_);
continue;
}
}
catch (const std::out_of_range &)
{
// Channel does not contain a special bit badge for this version
}
auto tooltip = (*badgeEmote)->tooltip.string;
// Use default bit badge
if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
"bits", cheerAmount))
if (badge.key_ == "bits")
{
this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity)
->setTooltip(tooltip);
const auto &cheerAmount = badge.value_;
tooltip = QString("Twitch cheer %0").arg(cheerAmount);
}
}
else if (badge == "staff/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.staff),
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Staff");
}
else if (badge == "admin/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.admin),
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Admin");
}
else if (badge == "global_mod/1")
{
this->emplace<ImageElement>(
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<ImageElement>(
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<ImageElement>(
Image::fromPixmap(app->resources->twitch.vip),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("VIP");
auto badgeInfoIt = badgeInfos.find(badge.key_);
if (badgeInfoIt != badgeInfos.end())
{
const auto &subMonths = badgeInfoIt->second;
tooltip += QString(" (%0 months)").arg(subMonths);
}
else if (badge == "broadcaster/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.broadcaster),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("Twitch Broadcaster");
}
else if (badge == "turbo/1")
{
this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.turbo),
MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Turbo Subscriber");
}
else if (badge == "premium/1")
{
this->emplace<ImageElement>(
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)
{
case 1:
{
this->emplace<ImageElement>(
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;
}
}
else if (badge.startsWith("subscriber/"))
{
if (auto badgeEmote = this->twitchChannel->twitchBadge(
"subscriber", badge.mid(11)))
{
this->emplace<BadgeElement>(
badgeEmote.get(), MessageElementFlag::BadgeSubscription)
->setTooltip((*badgeEmote)->tooltip.string);
continue;
}
// use default subscriber badge if custom one not found
this->emplace<ImageElement>(
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<BadgeElement>(badgeEmote.get(),
MessageElementFlag::BadgeVanity)
->setTooltip((*badgeEmote)->tooltip.string);
continue;
}
if (auto _badge = this->twitchChannel->globalTwitchBadges().badge(
splits[0], splits[1]))
{
this->emplace<BadgeElement>(_badge.get(),
MessageElementFlag::BadgeVanity)
->setTooltip((*_badge)->tooltip.string);
continue;
}
}
this->emplace<BadgeElement>(badgeEmote.get(), badge.flag_)
->setTooltip(tooltip);
}
}
@ -1312,12 +1264,34 @@ 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<EmoteElement>(cheerEmote.staticEmote,
@ -1330,9 +1304,45 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
}
if (cheerEmote.color != QColor())
{
this->emplace<TextElement>(this->bits, MessageElementFlag::BitsAmount,
this->emplace<TextElement>(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<EmoteElement>(cheerEmote.staticEmote,
MessageElementFlag::BitsStatic);
}
if (cheerEmote.animatedEmote)
{
this->emplace<EmoteElement>(cheerEmote.animatedEmote,
MessageElementFlag::BitsAnimated);
}
if (cheerEmote.color != QColor())
{
this->emplace<TextElement>(match.captured(1),
MessageElementFlag::BitsAmount,
cheerEmote.color);
}
return Success;
}
} // namespace chatterino

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include <IrcMessage>
#include <QString>
@ -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<EmotePtr> getTwitchBadge(const Badge &badge);
void appendTwitchEmote(
const QString &emote,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
@ -79,6 +81,8 @@ private:
QString roomID_;
bool hasBits_ = false;
QString bits;
int bitsLeft;
bool bitsStacked = false;
bool historicalMessage_ = false;
QString userId_;

View file

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

View file

@ -12,7 +12,7 @@
#include <array>
#include <unordered_map>
namespace AB_NAMESPACE {
namespace chatterino {
class Settings;
class Paths;
@ -90,4 +90,4 @@ private:
Fonts *getFonts();
} // namespace AB_NAMESPACE
} // namespace chatterino

View file

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

View file

@ -39,7 +39,7 @@ public:
private:
void initAppFilePathHash();
void initCheckPortable();
void initAppDataDirectory();
void initRootDirectory();
void initSubDirectories();
boost::optional<bool> portable_;

View file

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

View file

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

View file

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

View file

@ -1,25 +1,24 @@
#pragma once
#include "BaseSettings.hpp"
#include <pajlada/settings/setting.hpp>
#include <pajlada/settings/settinglistener.hpp>
#include "BaseSettings.hpp"
#include "common/Channel.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "controllers/moderationactions/ModerationAction.hpp"
#include "singletons/Toasts.hpp"
#include <pajlada/settings/setting.hpp>
#include <pajlada/settings/settinglistener.hpp>
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",

View file

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

View file

@ -134,8 +134,7 @@ public:
}
QDesktopServices::openUrl(QUrl(link));
break;
case ToastReaction::OpenInStreamlink:
{
case ToastReaction::OpenInStreamlink: {
openStreamlinkForChannel(channelName_);
break;
}

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