diff --git a/.CI/CreateDMG.sh b/.CI/CreateDMG.sh
index 60a8f5341..8ad191e2d 100755
--- a/.CI/CreateDMG.sh
+++ b/.CI/CreateDMG.sh
@@ -1,13 +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"
+/usr/local/opt/qt/bin/macdeployqt chatterino.app
+echo "Creating python3 virtual environment"
+python3 -m venv venv
+echo "Entering python3 virtual environment"
+. venv/bin/activate
+echo "Installing dmgbuild"
+python3 -m pip install dmgbuild
+echo "Running dmgbuild.."
+dmgbuild --settings ./../.CI/dmg-settings.py -D app=./chatterino.app Chatterino2 chatterino-osx.dmg
+echo "Done!"
diff --git a/.CI/dmg-settings.py b/.CI/dmg-settings.py
new file mode 100644
index 000000000..6b068fa1b
--- /dev/null
+++ b/.CI/dmg-settings.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import biplist
+import os.path
+
+#
+# Example settings file for dmgbuild
+#
+
+# Use like this: dmgbuild -s settings.py "Test Volume" test.dmg
+
+# You can actually use this file for your own application (not just TextEdit)
+# by doing e.g.
+#
+# dmgbuild -s settings.py -D app=/path/to/My.app "My Application" MyApp.dmg
+
+# .. Useful stuff ..............................................................
+
+application = defines.get('app', '/Applications/TextEdit.app')
+appname = os.path.basename(application)
+
+def icon_from_app(app_path):
+ plist_path = os.path.join(app_path, 'Contents', 'Info.plist')
+ plist = biplist.readPlist(plist_path)
+ icon_name = plist['CFBundleIconFile']
+ icon_root,icon_ext = os.path.splitext(icon_name)
+ if not icon_ext:
+ icon_ext = '.icns'
+ icon_name = icon_root + icon_ext
+ return os.path.join(app_path, 'Contents', 'Resources', icon_name)
+
+# .. Basics ....................................................................
+
+# Uncomment to override the output filename
+# filename = 'test.dmg'
+
+# Uncomment to override the output volume name
+# volume_name = 'Test'
+
+# Volume format (see hdiutil create -help)
+format = defines.get('format', 'UDBZ')
+
+# Volume size
+size = defines.get('size', None)
+
+# Files to include
+files = [ application ]
+
+# Symlinks to create
+symlinks = { 'Applications': '/Applications' }
+
+# Volume icon
+#
+# You can either define icon, in which case that icon file will be copied to the
+# image, *or* you can define badge_icon, in which case the icon file you specify
+# will be used to badge the system's Removable Disk icon
+#
+#icon = '/path/to/icon.icns'
+badge_icon = icon_from_app(application)
+
+# Where to put the icons
+icon_locations = {
+ appname: (140, 120),
+ 'Applications': (500, 120)
+ }
+
+# .. Window configuration ......................................................
+
+# Background
+#
+# This is a STRING containing any of the following:
+#
+# #3344ff - web-style RGB color
+# #34f - web-style RGB color, short form (#34f == #3344ff)
+# rgb(1,0,0) - RGB color, each value is between 0 and 1
+# hsl(120,1,.5) - HSL (hue saturation lightness) color
+# hwb(300,0,0) - HWB (hue whiteness blackness) color
+# cmyk(0,1,0,0) - CMYK color
+# goldenrod - X11/SVG named color
+# builtin-arrow - A simple built-in background with a blue arrow
+# /foo/bar/baz.png - The path to an image file
+#
+# The hue component in hsl() and hwb() may include a unit; it defaults to
+# degrees ('deg'), but also supports radians ('rad') and gradians ('grad'
+# or 'gon').
+#
+# Other color components may be expressed either in the range 0 to 1, or
+# as percentages (e.g. 60% is equivalent to 0.6).
+background = 'builtin-arrow'
+
+show_status_bar = False
+show_tab_view = False
+show_toolbar = False
+show_pathbar = False
+show_sidebar = False
+sidebar_width = 180
+
+# Window position in ((x, y), (w, h)) format
+window_rect = ((100, 100), (640, 280))
+
+# Select the default view; must be one of
+#
+# 'icon-view'
+# 'list-view'
+# 'column-view'
+# 'coverflow'
+#
+default_view = 'icon-view'
+
+# General view configuration
+show_icon_preview = False
+
+# Set these to True to force inclusion of icon/list view settings (otherwise
+# we only include settings for the default view)
+include_icon_view_settings = 'auto'
+include_list_view_settings = 'auto'
+
+# .. Icon view configuration ...................................................
+
+arrange_by = None
+grid_offset = (0, 0)
+grid_spacing = 100
+scroll_position = (0, 0)
+label_pos = 'bottom' # or 'right'
+text_size = 16
+icon_size = 128
+
+# .. List view configuration ...................................................
+
+# Column names are as follows:
+#
+# name
+# date-modified
+# date-created
+# date-added
+# date-last-opened
+# size
+# kind
+# label
+# version
+# comments
+#
+list_icon_size = 16
+list_text_size = 12
+list_scroll_position = (0, 0)
+list_sort_by = 'name'
+list_use_relative_dates = True
+list_calculate_all_sizes = False,
+list_columns = ('name', 'date-modified', 'size', 'kind', 'date-added')
+list_column_widths = {
+ 'name': 300,
+ 'date-modified': 181,
+ 'date-created': 181,
+ 'date-added': 181,
+ 'date-last-opened': 181,
+ 'size': 97,
+ 'kind': 115,
+ 'label': 100,
+ 'version': 75,
+ 'comments': 300,
+ }
+list_column_sort_directions = {
+ 'name': 'ascending',
+ 'date-modified': 'descending',
+ 'date-created': 'descending',
+ 'date-added': 'descending',
+ 'date-last-opened': 'descending',
+ 'size': 'descending',
+ 'kind': 'ascending',
+ 'label': 'ascending',
+ 'version': 'ascending',
+ 'comments': 'ascending',
+ }
+
+# .. License configuration .....................................................
+
+# Text in the license configuration is stored in the resources, which means
+# it gets stored in a legacy Mac encoding according to the language. dmgbuild
+# will *try* to convert Unicode strings to the appropriate encoding, *but*
+# you should be aware that Python doesn't support all of the necessary encodings;
+# in many cases you will need to encode the text yourself and use byte strings
+# instead here.
+
+# Recognized language names are:
+#
+# af_ZA, ar, be_BY, bg_BG, bn, bo, br, ca_ES, cs_CZ, cy, da_DK, de_AT, de_CH,
+# de_DE, dz_BT, el_CY, el_GR, en_AU, en_CA, en_GB, en_IE, en_SG, en_US, eo,
+# es_419, es_ES, et_EE, fa_IR, fi_FI, fo_FO, fr_001, fr_BE, fr_CA, fr_CH,
+# fr_FR, ga-Latg_IE, ga_IE, gd, grc, gu_IN, gv, he_IL, hi_IN, hr_HR, hu_HU,
+# hy_AM, is_IS, it_CH, it_IT, iu_CA, ja_JP, ka_GE, kl, ko_KR, lt_LT, lv_LV,
+# mk_MK, mr_IN, mt_MT, nb_NO, ne_NP, nl_BE, nl_NL, nn_NO, pa, pl_PL, pt_BR,
+# pt_PT, ro_RO, ru_RU, se, sk_SK, sl_SI, sr_RS, sv_SE, th_TH, to_TO, tr_TR,
+# uk_UA, ur_IN, ur_PK, uz_UZ, vi_VN, zh_CN, zh_TW
+
+# license = {
+# 'default-language': 'en_US',
+# 'licenses': {
+# # For each language, the text of the license. This can be plain text,
+# # RTF (in which case it must start "{\rtf1"), or a path to a file
+# # containing the license text. If you're using RTF,
+# # watch out for Python escaping (or read it from a file).
+# 'English': b'''{\\rtf1\\ansi\\ansicpg1252\\cocoartf1504\\cocoasubrtf820
+# {\\fonttbl\\f0\\fnil\\fcharset0 Helvetica-Bold;\\f1\\fnil\\fcharset0 Helvetica;}
+# {\\colortbl;\\red255\\green255\\blue255;\\red0\\green0\\blue0;}
+# {\\*\\expandedcolortbl;;\\cssrgb\\c0\\c0\\c0;}
+# \\paperw11905\\paperh16837\\margl1133\\margr1133\\margb1133\\margt1133
+# \\deftab720
+# \\pard\\pardeftab720\\sa160\\partightenfactor0
+
+# \\f0\\b\\fs60 \\cf2 \\expnd0\\expndtw0\\kerning0
+# \\up0 \\nosupersub \\ulnone \\outl0\\strokewidth0 \\strokec2 Test License\\
+# \\pard\\pardeftab720\\sa160\\partightenfactor0
+
+# \\fs36 \\cf2 \\strokec2 What is this?\\
+# \\pard\\pardeftab720\\sa160\\partightenfactor0
+
+# \\f1\\b0\\fs22 \\cf2 \\strokec2 This is the English license. It says what you are allowed to do with this software.\\
+# \\
+# }''',
+# },
+# 'buttons': {
+# # For each language, text for the buttons on the licensing window.
+# #
+# # Default buttons and text are built-in for the following languages:
+# #
+# # English (en_US), German (de_DE), Spanish (es_ES), French (fr_FR),
+# # Italian (it_IT), Japanese (ja_JP), Dutch (nl_NL), Swedish (sv_SE),
+# # Brazilian Portuguese (pt_BR), Simplified Chinese (zh_CN),
+# # Traditional Chinese (zh_TW), Danish (da_DK), Finnish (fi_FI),
+# # Korean (ko_KR), Norwegian (nb_NO)
+# #
+# # You don't need to specify them for those languages; if you fail to
+# # specify them for some other language, English will be used instead.
+
+# 'en_US': (
+# b'English',
+# b'Agree',
+# b'Disagree',
+# b'Print',
+# b'Save',
+# b'If you agree with the terms of this license, press "Agree" to '
+# b'install the software. If you do not agree, press "Disagree".'
+# ),
+# },
+# }
+
diff --git a/.github/ISSUE_TEMPLATE/feature-suggestion.md b/.github/ISSUE_TEMPLATE/feature-suggestion.md
index d44729381..38e6dd885 100644
--- a/.github/ISSUE_TEMPLATE/feature-suggestion.md
+++ b/.github/ISSUE_TEMPLATE/feature-suggestion.md
@@ -7,14 +7,8 @@ assignees: ''
---
-**Is your feature request related to a problem? Please describe.**
-
+**What should be added?**
+
-**Describe the solution you'd like**
-
-
-**Describe alternatives you've considered**
-
-
-**Additional context**
-
+**Why should it be added?**
+
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md
similarity index 100%
rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md
rename to .github/pull_request_template.md
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 75bdc29d5..e4ea6900a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,8 +24,18 @@ jobs:
with:
submodules: true
+ - name: Cache Qt
+ id: cache-qt
+ uses: actions/cache@v2
+ with:
+ path: ../Qt
+ key: ${{ runner.os }}-QtCache-v2
+
- name: Install Qt
- uses: jurplel/install-qt-action@v1
+ uses: jurplel/install-qt-action@v2
+ with:
+ mirror: 'http://mirrors.ocf.berkeley.edu/qt/'
+ cached: ${{ steps.cache-qt.outputs.cache-hit }}
# WINDOWS
- name: Cache conan
@@ -45,8 +55,7 @@ jobs:
- 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 install conan -y -s="https://api.bintray.com/nuget/anotherfoxguy/choco-pkg"
+ choco install conan -y
refreshenv
shell: cmd
diff --git a/.github/workflows/check-formatting.yml b/.github/workflows/check-formatting.yml
index 3c04502b4..aa7332984 100644
--- a/.github/workflows/check-formatting.yml
+++ b/.github/workflows/check-formatting.yml
@@ -1,6 +1,14 @@
name: Check formatting
-on: [push, pull_request]
+on:
+ push:
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ pull_request:
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
jobs:
build:
diff --git a/BUILDING_ON_FREEBSD.md b/BUILDING_ON_FREEBSD.md
new file mode 100644
index 000000000..796317931
--- /dev/null
+++ b/BUILDING_ON_FREEBSD.md
@@ -0,0 +1,17 @@
+# FreeBSD
+
+Note on Qt version compatibility: If you are installing Qt from a package manager, please ensure the version you are installing is at least **Qt 5.10 or newer**.
+
+## FreeBSD 12.1-RELEASE
+
+Note: This is known to work on FreeBSD 12.1-RELEASE amd64. Chances are
+high that this also works on older FreeBSD releases, architectures and
+FreeBSD 13.0-CURRENT.
+
+1. Install build dependencies from package sources (or build from the
+ ports tree): `# pkg install qt5-core qt5-multimedia qt5-svg
+ qt5-qmake qt5-buildtools gstreamer-plugins-good boost-libs
+ rapidjson`
+1. go into project directory
+1. create build folder `$ mkdir build && cd build`
+1. `$ qmake .. && make`
diff --git a/BUILDING_ON_WINDOWS.md b/BUILDING_ON_WINDOWS.md
index 40df39bb0..b456efb44 100644
--- a/BUILDING_ON_WINDOWS.md
+++ b/BUILDING_ON_WINDOWS.md
@@ -29,18 +29,20 @@ Note: This installation will take about 1.5 GB of disk space.
## OpenSSL
### For our websocket library, we need OpenSSL 1.1
-1. Download OpenSSL for windows, version `1.1.0j`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_0j.exe)**
+1. Download OpenSSL for windows, version `1.1.1g`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1g.exe)**
2. When prompted, install OpenSSL to `C:\local\openssl`
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
### For Qt SSL, we need OpenSSL 1.0
-1. Download OpenSSL for windows, version `1.0.2r`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2r.exe)**
+1. Download OpenSSL for windows, version `1.0.2u`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_0_2u.exe)**
2. When prompted, install it to any arbitrary empty directory.
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
4. Copy the OpenSSL 1.0 files from its `\bin` folder to `C:\local\bin` (You will need to create the folder)
5. Then copy the OpenSSL 1.1 files from its `\bin` folder to `C:\local\bin` (Overwrite any duplicate files)
6. Add `C:\local\bin` to your path folder ([Follow guide here if you don't know how to do it]( https://www.computerhope.com/issues/ch000549.htm#windows8))
+**If the download links above do not work, try downloading similar 1.1.x & 1.0.x versions [here](https://slproweb.com/products/Win32OpenSSL.html). Note: Don't download the "light" installers, they do not have the required files.**
+
Note: This installation will take about 200 MB of disk space.
## Qt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 201b0be80..0774df79d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
## Unversioned
+
+- Major: We now support image thumbnails coming from the link resolver. This feature is off by default and can be enabled in the settings with the "Show link thumbnail" setting. This feature also requires the "Show link info when hovering" setting to be enabled (#1664)
+- Major: Added image upload functionality to i.nuuls.com with an ability to change upload destination. This works by dragging and dropping an image into a split, or pasting an image into the text edit field. (#1332, #1741)
+- Minor: Clicking on `Open in browser` in a whisper split will now open your whispers on twitch. (#1828)
+- Minor: Clicking on @mentions will open the User Popup. (#1674)
+- Minor: You can now open the Twitch User Card by middle-mouse clicking a username. (#1669)
+- Minor: User Popup now also includes recent user messages (#1729)
+- Minor: BetterTTV / FrankerFaceZ emote tooltips now also have emote authors' name (#1721)
- Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549)
+- Minor: Removed "Online Logs" functionality as services are shut down (#1640)
+- Minor: CTRL+F now selects the Find text input field in the Settings Dialog (#1806 #1811)
+- Minor: CTRL+F now selects the search text input field in the Search Popup (#1812)
- Bugfix: Fix preview on hover not working when Animated emotes options was disabled (#1546)
+- Bugfix: FFZ custom mod badges no longer scale with the emote scale options (#1602)
+- Bugfix: MacOS updater looked for non-existing fields, causing it to always fail the update check (#1642)
+- Bugfix: Fixed message menu crashing if the message you right-clicked goes out of scope before you select an action (#1783) (#1787)
+- Bugfix: Fixed alternate messages flickering in UserInfoPopup when clicking Refresh if there was an odd number of messages in there (#1789 #1810)
+- Settings open faster
+- Dev: Fully remove Twitch Chatroom support
+- Dev: Handle conversion of historical CLEARCHAT messages to NOTICE messages in Chatterino instead of relying on the Recent Messages API to handle it for us. (#1804)
diff --git a/README.md b/README.md
index a0fc71be7..f39e14427 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@ git submodule update --init --recursive
[Building on Mac](../master/BUILDING_ON_MAC.md)
+[Building on FreeBSD](../master/BUILDING_ON_FREEBSD.md)
+
## Code style
The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format.
diff --git a/.travis.yml b/_.travis.yml
similarity index 100%
rename from .travis.yml
rename to _.travis.yml
diff --git a/chatterino.pro b/chatterino.pro
index 6cf3d8128..bd34241de 100644
--- a/chatterino.pro
+++ b/chatterino.pro
@@ -131,21 +131,16 @@ SOURCES += \
src/controllers/commands/CommandController.cpp \
src/controllers/commands/CommandModel.cpp \
src/controllers/highlights/HighlightBlacklistModel.cpp \
- src/controllers/highlights/HighlightController.cpp \
src/controllers/highlights/HighlightModel.cpp \
src/controllers/highlights/HighlightPhrase.cpp \
src/controllers/highlights/UserHighlightModel.cpp \
- src/controllers/ignores/IgnoreController.cpp \
src/controllers/ignores/IgnoreModel.cpp \
src/controllers/moderationactions/ModerationAction.cpp \
src/controllers/moderationactions/ModerationActionModel.cpp \
- src/controllers/moderationactions/ModerationActions.cpp \
src/controllers/notifications/NotificationController.cpp \
src/controllers/notifications/NotificationModel.cpp \
- src/controllers/pings/PingController.cpp \
- src/controllers/pings/PingModel.cpp \
+ src/controllers/pings/MutedChannelModel.cpp \
src/controllers/taggedusers/TaggedUser.cpp \
- src/controllers/taggedusers/TaggedUsersController.cpp \
src/controllers/taggedusers/TaggedUsersModel.cpp \
src/debug/Benchmark.cpp \
src/main.cpp \
@@ -161,6 +156,7 @@ SOURCES += \
src/messages/MessageColor.cpp \
src/messages/MessageContainer.cpp \
src/messages/MessageElement.cpp \
+ src/messages/SharedMessageBuilder.cpp \
src/messages/search/AuthorPredicate.cpp \
src/messages/search/LinkPredicate.cpp \
src/messages/search/SubstringPredicate.cpp \
@@ -176,17 +172,18 @@ SOURCES += \
src/providers/irc/IrcChannel2.cpp \
src/providers/irc/IrcCommands.cpp \
src/providers/irc/IrcConnection2.cpp \
+ src/providers/irc/IrcMessageBuilder.cpp \
src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \
- src/providers/twitch/ChatroomChannel.cpp \
+ src/providers/twitch/ChannelPointReward.cpp \
+ src/providers/twitch/api/Helix.cpp \
+ src/providers/twitch/api/Kraken.cpp \
src/providers/twitch/IrcMessageHandler.cpp \
- src/providers/twitch/PartialTwitchUser.cpp \
src/providers/twitch/PubsubActions.cpp \
src/providers/twitch/PubsubClient.cpp \
src/providers/twitch/PubsubHelpers.cpp \
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 \
@@ -223,6 +220,9 @@ SOURCES += \
src/util/JsonQuery.cpp \
src/util/RapidjsonHelpers.cpp \
src/util/StreamLink.cpp \
+ src/util/StreamerMode.cpp \
+ src/util/Twitch.cpp \
+ src/util/NuulsUploader.cpp \
src/util/WindowsHelper.cpp \
src/widgets/AccountSwitchPopup.cpp \
src/widgets/AccountSwitchWidget.cpp \
@@ -235,7 +235,6 @@ SOURCES += \
src/widgets/dialogs/IrcConnectionEditor.cpp \
src/widgets/dialogs/LastRunCrashDialog.cpp \
src/widgets/dialogs/LoginDialog.cpp \
- src/widgets/dialogs/LogsPopup.cpp \
src/widgets/dialogs/NotificationPopup.cpp \
src/widgets/dialogs/QualityPopup.cpp \
src/widgets/dialogs/SelectChannelDialog.cpp \
@@ -303,6 +302,7 @@ HEADERS += \
src/common/DownloadManager.hpp \
src/common/Env.hpp \
src/common/FlagsEnum.hpp \
+ src/common/IrcColors.hpp \
src/common/LinkParser.hpp \
src/common/Modes.hpp \
src/common/NetworkCommon.hpp \
@@ -327,7 +327,6 @@ HEADERS += \
src/controllers/commands/CommandModel.hpp \
src/controllers/highlights/HighlightBlacklistModel.hpp \
src/controllers/highlights/HighlightBlacklistUser.hpp \
- src/controllers/highlights/HighlightController.hpp \
src/controllers/highlights/HighlightModel.hpp \
src/controllers/highlights/HighlightPhrase.hpp \
src/controllers/highlights/UserHighlightModel.hpp \
@@ -336,13 +335,10 @@ HEADERS += \
src/controllers/ignores/IgnorePhrase.hpp \
src/controllers/moderationactions/ModerationAction.hpp \
src/controllers/moderationactions/ModerationActionModel.hpp \
- src/controllers/moderationactions/ModerationActions.hpp \
src/controllers/notifications/NotificationController.hpp \
src/controllers/notifications/NotificationModel.hpp \
- src/controllers/pings/PingController.hpp \
- src/controllers/pings/PingModel.hpp \
+ src/controllers/pings/MutedChannelModel.hpp \
src/controllers/taggedusers/TaggedUser.hpp \
- src/controllers/taggedusers/TaggedUsersController.hpp \
src/controllers/taggedusers/TaggedUsersModel.hpp \
src/debug/AssertInGuiThread.hpp \
src/debug/Benchmark.hpp \
@@ -362,6 +358,7 @@ HEADERS += \
src/messages/MessageContainer.hpp \
src/messages/MessageElement.hpp \
src/messages/MessageParseArgs.hpp \
+ src/messages/SharedMessageBuilder.hpp \
src/messages/search/AuthorPredicate.hpp \
src/messages/search/LinkPredicate.hpp \
src/messages/search/MessagePredicate.hpp \
@@ -380,18 +377,19 @@ HEADERS += \
src/providers/irc/IrcChannel2.hpp \
src/providers/irc/IrcCommands.hpp \
src/providers/irc/IrcConnection2.hpp \
+ src/providers/irc/IrcMessageBuilder.hpp \
src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \
- src/providers/twitch/ChatroomChannel.hpp \
+ src/providers/twitch/ChannelPointReward.hpp \
+ src/providers/twitch/api/Helix.hpp \
+ src/providers/twitch/api/Kraken.hpp \
src/providers/twitch/EmoteValue.hpp \
src/providers/twitch/IrcMessageHandler.hpp \
- src/providers/twitch/PartialTwitchUser.hpp \
src/providers/twitch/PubsubActions.hpp \
src/providers/twitch/PubsubClient.hpp \
src/providers/twitch/PubsubHelpers.hpp \
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 \
@@ -436,9 +434,12 @@ HEADERS += \
src/util/LayoutCreator.hpp \
src/util/LayoutHelper.hpp \
src/util/Overloaded.hpp \
+ src/util/PersistSignalVector.hpp \
src/util/PostToThread.hpp \
src/util/QObjectRef.hpp \
src/util/QStringHash.hpp \
+ src/util/StreamerMode.hpp \
+ src/util/Twitch.hpp \
src/util/rangealgorithm.hpp \
src/util/RapidjsonHelpers.hpp \
src/util/RapidJsonSerializeQString.hpp \
@@ -449,6 +450,7 @@ HEADERS += \
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 \
@@ -461,7 +463,6 @@ HEADERS += \
src/widgets/dialogs/IrcConnectionEditor.hpp \
src/widgets/dialogs/LastRunCrashDialog.hpp \
src/widgets/dialogs/LoginDialog.hpp \
- src/widgets/dialogs/LogsPopup.hpp \
src/widgets/dialogs/NotificationPopup.hpp \
src/widgets/dialogs/QualityPopup.hpp \
src/widgets/dialogs/SelectChannelDialog.hpp \
diff --git a/docs/Commands.md b/docs/Commands.md
new file mode 100644
index 000000000..ceaebb831
--- /dev/null
+++ b/docs/Commands.md
@@ -0,0 +1,20 @@
+Commands are used as shortcuts for long messages. If a message starts with the "trigger" then the message will be replaced with the Command.
+
+#### Example
+Add Command `Hello chat :)` with the trigger `/hello`. Now typing `/hello` in chat will send `Hello chat :)` instead of `/hello`.
+
+## Advanced
+
+- The trigger has to be matched at the **start** of the message but there is a setting to also match them at the **end**.
+- Triggers don't need to start with `/`
+
+#### Using placeholders
+- `{1}`, `{2}`, `{3}` and so on can be used to insert the 1st, 2nd, 3rd, ... word after the trigger.
+
+ Example: Add Command `/timeout {1} 1` with trigger `/warn`. Now typing `/warn user123` will send `/timeout user123 1`
+
+- Similarly `{1+}` and so on can be used to insert all words starting with the 1st, ... word.
+
+ Example: Add Command `Have a {1+} day!` with trigger `/day`. Now typing `/day very super nice` will send `Have a very super nice day!`
+
+- You can use `{{1}` if you want to send `{1}` literally.
\ No newline at end of file
diff --git a/docs/ENV.md b/docs/ENV.md
index f95c74286..a6efcea52 100644
--- a/docs/ENV.md
+++ b/docs/ENV.md
@@ -3,7 +3,7 @@ Below I have tried to list all environment variables that can be used to modify
### CHATTERINO2_RECENT_MESSAGES_URL
Used to change the URL that Chatterino2 uses when trying to load historic Twitch chat messages (if the setting is enabled).
-Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1?clearchatToNotice=true` (an [open-source service](https://github.com/robotty/recent-messages) written and currently run by [@RAnders00](https://github.com/RAnders00))
+Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1` (an [open-source service](https://github.com/robotty/recent-messages2) written and currently run by [@RAnders00](https://github.com/RAnders00) - [visit the homepage for more details about the service](https://recent-messages.robotty.de/))
Arguments:
- `%1` = Name of the Twitch channel
diff --git a/docs/IMAGEUPLOADER.md b/docs/IMAGEUPLOADER.md
new file mode 100644
index 000000000..fec670ff8
--- /dev/null
+++ b/docs/IMAGEUPLOADER.md
@@ -0,0 +1,47 @@
+## Image Uploader
+You can drag and drop images to Chatterino or paste them from clipboard to upload them to an external service.
+
+By default, images are uploaded to [i.nuuls.com](https://i.nuuls.com).
+You can change that in `Chatterino Settings -> External Tools -> Image Uploader`.
+
+Note to advanced users: This module sends multipart-form requests via POST method, so uploading via SFTP/FTP won't work.
+However, popular hosts like [imgur.com](https://imgur.com) are [s-ul.eu](https://s-ul.eu) supported. Scroll down to see example cofiguration.
+
+### General settings explanation:
+
+|Row|Description|
+|-|-|
+|Request URL|Link to an API endpoint, which is requested by chatterino. Any needed URL parameters should be included here.|
+|Form field|Name of a field, which contains image data.|
+|Extra headers|Extra headers, that will be included in the request. Header name and value must be separated by colon (`:`). Multiple headers need to be separated with semicolons (`;`).
Example: `Authorization: supaKey ; NextHeader: value` .|
+|Image link|Schema that tells where is the link in service's response. Leave empty if server's response is just the link itself. Refer to json properties by `{property}`. Supports dot-notation, example: `{property.anotherProperty}` .|
+|Deletion link|Same as above.|
+
+
+
+## Examples
+
+### i.nuuls.com
+
+Simply clear all the fields.
+
+### imgur.com
+
+|Row|Description|
+|-|-|
+|Request URL|`https://api.imgur.com/3/image`|
+|Form field|`image`|
+|Extra headers|`Authorization: Client-ID c898c0bb848ca39`|
+|Image link|`{data.link}`|
+|Deletion link|`https://imgur.com/delete/{data.deletehash}`|
+
+### s-ul.eu
+
+Replace `XXXXXXXXXXXXXXX` with your API key from s-ul.eu. It can be found on [your account's configuration page](https://s-ul.eu/account/configurations).
+|Row|Description|
+|-|-|
+|Request URL|`https://s-ul.eu/api/v1/upload?wizard=true&key=XXXXXXXXXXXXXXX`|
+|Form field|`file`|
+|Extra headers||
+|Image link|`{url}`|
+|Deletion link|`https://s-ul.eu/delete.php?file={filename}&key=XXXXXXXXXXXXXXX`|
diff --git a/docs/README.md b/docs/README.md
index 57a48fe0f..5ae4760f8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1 +1,21 @@
# Documents
+
+Welcome to the Chatterino2 documentation!
+
+Table of contents:
+ - [Environment variables](ENV.md)
+ - [Commands](Commands.md)
+ - [Regex](Regex.md)
+ - [Notes](notes/README.md)
+ - [TCaccountmanager](notes/TCaccountmanager.md)
+ - [TCappearanceSettings.md](notes/TCappearanceSettings.md)
+ - [TCchannelNavigation.md](notes/TCchannelNavigation.md)
+ - [TCchatterinoFeatures.md](notes/TCchatterinoFeatures.md)
+ - [TCchatting.md](notes/TCchatting.md)
+ - [TCchatWindowNavigation.md](notes/TCchatWindowNavigation.md)
+ - [TCemotes.md](notes/TCemotes.md)
+ - [TCgeneral.md](notes/TCgeneral.md)
+ - [TChighlighting.md](notes/TChighlighting.md)
+ - [TCtabsAndSplits.md](notes/TCtabsAndSplits.md)
+ - [TCusernameTabbing.md](notes/TCusernameTabbing.md)
+
diff --git a/docs/Regex.md b/docs/Regex.md
new file mode 100644
index 000000000..9ef784a5c
--- /dev/null
+++ b/docs/Regex.md
@@ -0,0 +1,45 @@
+_Regular expressions_ (or short _regexes_) are often used to check if a text matches a certain pattern. For example the regex `ab?c` would match `abc` or `ac`, but not `abbc` or `123`. In Chatterino, you can use them to highlight messages (and more) based on complex conditions.
+
+Basic patterns:
+
+|Pattern |Matches|
+|-|-|
+|`x?` |nothing or `x`|
+|`x*` |`x`, repeated any number of times|
+|`x+` |`x`, repeated any number of times but at least 1|
+|`^` |The start of the text|
+|`$` |The end of the text|
+|`x\|y` |`x` or `y`|
+
+You can group multiple statements with `()`:
+
+|Pattern |Matches|
+|-|-|
+|`asd?` |`asd` or `as`|
+|`(asd)?` |`asd` or nothing|
+|`\(asd\)` |`(asd)`, literally|
+
+You can also group multiple characters with `[]`:
+
+|Pattern |Matches|
+|-|-|
+|`[xyz]` |`x`, `y` or `z`|
+|`[1-5a-f]` |`1`,`2`,`3`,`4`,`5`,`a`,`b`,`c`,`d`,`e`,`f`|
+|`[^abc]` |Anything, **except** `a`, `b` and `c`|
+|`[\-]` |`-`, literally (escaped with `\`)|
+|`\[xyz\]` |`[xyz]`, literally|
+
+Special patterns:
+
+|Pattern |Matches|
+|-|-|
+|`\d` |Digit characters (0-9)|
+|`\D` |Non-digit characters|
+|`\w` |Word characters (a-zA-Z0-9_)|
+|`\W` |Non-word characters|
+|`\s` |Spaces, tabs, etc.|
+|`\S` |Not spaces, tabs, etc.|
+|`\b` |Word boundaries (between \w and \W)|
+|`\B` |Non-word boundaries|
+
+You can try out your regex pattern on websites like [https://regex101.com/](regex101.com).
\ No newline at end of file
diff --git a/lib/signals b/lib/signals
index 1c38746b0..6665ccad9 160000
--- a/lib/signals
+++ b/lib/signals
@@ -1 +1 @@
-Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be
+Subproject commit 6665ccad90461c01b7fe704a98a835953d644156
diff --git a/resources/contributors.txt b/resources/contributors.txt
index ea0644a7c..cee2ae135 100644
--- a/resources/contributors.txt
+++ b/resources/contributors.txt
@@ -29,6 +29,8 @@ TranRed | https://github.com/TranRed | | Contributor
RAnders00 | https://github.com/RAnders00 | | Contributor
YungLPR | https://github.com/leon-richardt | | Contributor
Mm2PL | https://github.com/mm2pl | | Contributor
+gempir | https://github.com/gempir | | Contributor
+mfmarlow | https://github.com/mfmarlow | | Contributor
# If you are a contributor add yourself above this line
Defman21 | https://github.com/Defman21 | | Documentation
diff --git a/resources/qss/settings.qss b/resources/qss/settings.qss
index 590dff5f5..a3172fd65 100644
--- a/resources/qss/settings.qss
+++ b/resources/qss/settings.qss
@@ -39,7 +39,6 @@ chatterino--TitleLabel {
font-family: "Segoe UI light";
font-size: 24px;
color: #4FC3F7;
- margin-top: 16px;
}
chatterino--DescriptionLabel {
diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc
index dbb639bbc..4fe4cf72e 100644
--- a/resources/resources_autogenerated.qrc
+++ b/resources/resources_autogenerated.qrc
@@ -28,8 +28,9 @@
buttons/unmod.png
buttons/update.png
buttons/updateError.png
- com.chatterino.chatterino.desktop
chatterino.icns
+ com.chatterino.chatterino.appdata.xml
+ com.chatterino.chatterino.desktop
contributors.txt
emoji.json
emojidata.txt
@@ -50,6 +51,12 @@
licenses/websocketpp.txt
pajaDank.png
qss/settings.qss
+ scrolling/downScroll.png
+ scrolling/downScroll.svg
+ scrolling/neutralScroll.png
+ scrolling/neutralScroll.svg
+ scrolling/upScroll.png
+ scrolling/upScroll.svg
settings/about.svg
settings/aboutlogo.png
settings/accounts.svg
diff --git a/resources/scrolling/downScroll.png b/resources/scrolling/downScroll.png
new file mode 100644
index 000000000..cc46bb5c8
Binary files /dev/null and b/resources/scrolling/downScroll.png differ
diff --git a/resources/scrolling/downScroll.svg b/resources/scrolling/downScroll.svg
new file mode 100644
index 000000000..d2e8992ac
--- /dev/null
+++ b/resources/scrolling/downScroll.svg
@@ -0,0 +1,96 @@
+
+
+
+
diff --git a/resources/scrolling/neutralScroll.png b/resources/scrolling/neutralScroll.png
new file mode 100644
index 000000000..99ad1e7f2
Binary files /dev/null and b/resources/scrolling/neutralScroll.png differ
diff --git a/resources/scrolling/neutralScroll.svg b/resources/scrolling/neutralScroll.svg
new file mode 100644
index 000000000..6ad76c8ca
--- /dev/null
+++ b/resources/scrolling/neutralScroll.svg
@@ -0,0 +1,122 @@
+
+
+
+
diff --git a/resources/scrolling/upScroll.png b/resources/scrolling/upScroll.png
new file mode 100644
index 000000000..433642978
Binary files /dev/null and b/resources/scrolling/upScroll.png differ
diff --git a/resources/scrolling/upScroll.svg b/resources/scrolling/upScroll.svg
new file mode 100644
index 000000000..d25c3f10a
--- /dev/null
+++ b/resources/scrolling/upScroll.svg
@@ -0,0 +1,96 @@
+
+
+
+
diff --git a/src/Application.cpp b/src/Application.cpp
index fe0aab349..814ba92f5 100644
--- a/src/Application.cpp
+++ b/src/Application.cpp
@@ -5,12 +5,8 @@
#include "common/Args.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandController.hpp"
-#include "controllers/highlights/HighlightController.hpp"
#include "controllers/ignores/IgnoreController.hpp"
-#include "controllers/moderationactions/ModerationActions.hpp"
#include "controllers/notifications/NotificationController.hpp"
-#include "controllers/pings/PingController.hpp"
-#include "controllers/taggedusers/TaggedUsersController.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
@@ -31,6 +27,7 @@
#include "singletons/WindowManager.hpp"
#include "util/IsBigEndian.hpp"
#include "util/PostToThread.hpp"
+#include "util/RapidjsonHelpers.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
#include "widgets/splits/Split.hpp"
@@ -54,16 +51,10 @@ Application::Application(Settings &_settings, Paths &_paths)
, accounts(&this->emplace())
, commands(&this->emplace())
- , highlights(&this->emplace())
, notifications(&this->emplace())
- , pings(&this->emplace())
- , ignores(&this->emplace())
- , taggedUsers(&this->emplace())
- , moderationActions(&this->emplace())
, twitch2(&this->emplace())
, chatterinoBadges(&this->emplace())
, logging(&this->emplace())
-
{
this->instance = this;
@@ -115,9 +106,6 @@ void Application::initialize(Settings &settings, Paths &paths)
this->initNm(paths);
this->initPubsub();
-
- this->moderationActions->items.delayedItemsChanged.connect(
- [this] { this->windows->forceLayoutChannelViews(); });
}
int Application::run(QApplication &qtApp)
@@ -130,6 +118,13 @@ int Application::run(QApplication &qtApp)
getSettings()->betaUpdates.connect(
[] { Updates::instance().checkForUpdates(); }, false);
+ getSettings()->moderationActions.delayedItemsChanged.connect(
+ [this] { this->windows->forceLayoutChannelViews(); });
+
+ getSettings()->highlightedMessages.delayedItemsChanged.connect(
+ [this] { this->windows->forceLayoutChannelViews(); });
+ getSettings()->highlightedUsers.delayedItemsChanged.connect(
+ [this] { this->windows->forceLayoutChannelViews(); });
return qtApp.exec();
}
@@ -156,14 +151,6 @@ void Application::initNm(Paths &paths)
void Application::initPubsub()
{
- this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) {
- qDebug() << "WHISPER SENT LOL"; //
- });
-
- this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) {
- qDebug() << "WHISPER RECEIVED LOL"; //
- });
-
this->twitch.pubsub->signals_.moderation.chatCleared.connect(
[this](const auto &action) {
auto chan =
@@ -298,6 +285,21 @@ void Application::initPubsub()
chan->deleteMessage(msg->id);
});
+ this->twitch.pubsub->signals_.pointReward.redeemed.connect([&](auto &data) {
+ QString channelId;
+ if (rj::getSafe(data, "channel_id", channelId))
+ {
+ const auto &chan =
+ this->twitch.server->getChannelOrEmptyByID(channelId);
+ auto channel = dynamic_cast(chan.get());
+ channel->addChannelPointReward(ChannelPointReward(data));
+ }
+ else
+ {
+ qDebug() << "Couldn't find channel id of point reward";
+ }
+ });
+
this->twitch.pubsub->start();
auto RequestModerationActions = [=]() {
diff --git a/src/Application.hpp b/src/Application.hpp
index fac8ec0f6..0c0e54bcb 100644
--- a/src/Application.hpp
+++ b/src/Application.hpp
@@ -3,6 +3,7 @@
#include
#include
+#include "common/SignalVector.hpp"
#include "common/Singleton.hpp"
#include "singletons/NativeMessaging.hpp"
@@ -12,13 +13,8 @@ class TwitchIrcServer;
class PubSub;
class CommandController;
-class HighlightController;
-class IgnoreController;
-class TaggedUsersController;
class AccountController;
-class ModerationActions;
class NotificationController;
-class PingController;
class Theme;
class WindowManager;
@@ -58,12 +54,7 @@ public:
AccountController *const accounts{};
CommandController *const commands{};
- HighlightController *const highlights{};
NotificationController *const notifications{};
- PingController *const pings{};
- IgnoreController *const ignores{};
- TaggedUsersController *const taggedUsers{};
- ModerationActions *const moderationActions{};
TwitchIrcServer *const twitch2{};
ChatterinoBadges *const chatterinoBadges{};
diff --git a/src/BaseTheme.cpp b/src/BaseTheme.cpp
index 99ddddb24..306f13f5c 100644
--- a/src/BaseTheme.cpp
+++ b/src/BaseTheme.cpp
@@ -159,21 +159,6 @@ void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier)
this->messages.backgrounds.regular = getColor(0, sat, 1);
this->messages.backgrounds.alternate = getColor(0, sat, 0.96);
- if (isLight_)
- {
- this->messages.backgrounds.highlighted =
- blendColors(themeColor, this->messages.backgrounds.regular, 0.8);
- }
- else
- {
- // REMOVED
- // this->messages.backgrounds.highlighted =
- // QColor(getSettings()->highlightColor);
- }
-
- this->messages.backgrounds.subscription =
- blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7);
-
// this->messages.backgrounds.resub
// this->messages.backgrounds.whisper
this->messages.disabled = getColor(0, sat, 1, 0.6);
diff --git a/src/BaseTheme.hpp b/src/BaseTheme.hpp
index d99808133..36e14c07f 100644
--- a/src/BaseTheme.hpp
+++ b/src/BaseTheme.hpp
@@ -64,8 +64,6 @@ public:
struct {
QColor regular;
QColor alternate;
- QColor highlighted;
- QColor subscription;
// QColor whisper;
} backgrounds;
diff --git a/src/RunGui.cpp b/src/RunGui.cpp
index 3088b6da1..e025d7f21 100644
--- a/src/RunGui.cpp
+++ b/src/RunGui.cpp
@@ -154,11 +154,6 @@ namespace {
toBeRemoved << info.absoluteFilePath();
}
}
-
- for (auto &&path : toBeRemoved)
- {
- qDebug() << path << QFile(path).remove();
- }
}
} // namespace
diff --git a/src/autogenerated/ResourcesAutogen.cpp b/src/autogenerated/ResourcesAutogen.cpp
index e7aab5d55..b0e5e6ce8 100644
--- a/src/autogenerated/ResourcesAutogen.cpp
+++ b/src/autogenerated/ResourcesAutogen.cpp
@@ -30,6 +30,9 @@ Resources2::Resources2()
this->error = QPixmap(":/error.png");
this->icon = QPixmap(":/icon.png");
this->pajaDank = QPixmap(":/pajaDank.png");
+ this->scrolling.downScroll = QPixmap(":/scrolling/downScroll.png");
+ this->scrolling.neutralScroll = QPixmap(":/scrolling/neutralScroll.png");
+ this->scrolling.upScroll = QPixmap(":/scrolling/upScroll.png");
this->settings.aboutlogo = QPixmap(":/settings/aboutlogo.png");
this->split.down = QPixmap(":/split/down.png");
this->split.left = QPixmap(":/split/left.png");
@@ -50,4 +53,4 @@ Resources2::Resources2()
this->twitch.vip = QPixmap(":/twitch/vip.png");
}
-} // namespace chatterino
+} // namespace chatterino
\ No newline at end of file
diff --git a/src/autogenerated/ResourcesAutogen.hpp b/src/autogenerated/ResourcesAutogen.hpp
index cb3423631..cbd6c0bfb 100644
--- a/src/autogenerated/ResourcesAutogen.hpp
+++ b/src/autogenerated/ResourcesAutogen.hpp
@@ -1,5 +1,4 @@
#include
-
#include "common/Singleton.hpp"
namespace chatterino {
@@ -39,6 +38,11 @@ public:
QPixmap error;
QPixmap icon;
QPixmap pajaDank;
+ struct {
+ QPixmap downScroll;
+ QPixmap neutralScroll;
+ QPixmap upScroll;
+ } scrolling;
struct {
QPixmap aboutlogo;
} settings;
@@ -65,4 +69,4 @@ public:
} twitch;
};
-} // namespace chatterino
+} // namespace chatterino
\ No newline at end of file
diff --git a/src/common/Aliases.hpp b/src/common/Aliases.hpp
index 2dea876b7..36fbc94ff 100644
--- a/src/common/Aliases.hpp
+++ b/src/common/Aliases.hpp
@@ -34,3 +34,4 @@ QStringAlias(Url);
QStringAlias(Tooltip);
QStringAlias(EmoteId);
QStringAlias(EmoteName);
+QStringAlias(EmoteAuthor);
diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp
index 43e88ee38..3ff35385b 100644
--- a/src/common/Channel.cpp
+++ b/src/common/Channel.cpp
@@ -59,6 +59,11 @@ bool Channel::isEmpty() const
return this->name_.isEmpty();
}
+bool Channel::hasMessages() const
+{
+ return !this->messages_.empty();
+}
+
LimitedQueueSnapshot Channel::getMessageSnapshot()
{
return this->messages_.getSnapshot();
@@ -143,9 +148,8 @@ void Channel::addOrReplaceTimeout(MessagePtr message)
int count = s->count + 1;
- MessageBuilder replacement(systemMessage,
- message->searchText + QString(" (") +
- QString::number(count) + " times)");
+ MessageBuilder replacement(timeoutMessage, message->searchText,
+ count);
replacement->timeoutUser = message->timeoutUser;
replacement->count = count;
diff --git a/src/common/Channel.hpp b/src/common/Channel.hpp
index f484235ac..71eac8cd4 100644
--- a/src/common/Channel.hpp
+++ b/src/common/Channel.hpp
@@ -75,6 +75,8 @@ public:
void deleteMessage(QString messageID);
void clearMessages();
+ bool hasMessages() const;
+
QStringList modList;
// CHANNEL INFO
diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp
index 6cb3bc800..f3d368da8 100644
--- a/src/common/CompletionModel.cpp
+++ b/src/common/CompletionModel.cpp
@@ -127,7 +127,7 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
TaggedString::Type::Username);
}
}
- else
+ else if (!getSettings()->userCompletionOnlyWithAt)
{
for (const auto &name :
usernames->subrange(Prefix(usernamePrefix)))
diff --git a/src/common/Env.cpp b/src/common/Env.cpp
index 1fa819c3d..724c8d92a 100644
--- a/src/common/Env.cpp
+++ b/src/common/Env.cpp
@@ -50,7 +50,7 @@ Env::Env()
: recentMessagesApiUrl(
readStringEnv("CHATTERINO2_RECENT_MESSAGES_URL",
"https://recent-messages.robotty.de/api/v2/"
- "recent-messages/%1?clearchatToNotice=true"))
+ "recent-messages/%1"))
, linkResolverUrl(readStringEnv(
"CHATTERINO2_LINK_RESOLVER_URL",
"https://braize.pajlada.com/chatterino/link_resolver/%1"))
diff --git a/src/common/IrcColors.hpp b/src/common/IrcColors.hpp
new file mode 100644
index 000000000..972110565
--- /dev/null
+++ b/src/common/IrcColors.hpp
@@ -0,0 +1,62 @@
+#pragma once
+
+#include
+#include
+
+namespace chatterino {
+
+// Colors taken from https://modern.ircdocs.horse/formatting.html
+static QMap IRC_COLORS = {
+ {0, QColor("white")}, {1, QColor("black")},
+ {2, QColor("blue")}, {3, QColor("green")},
+ {4, QColor("red")}, {5, QColor("brown")},
+ {6, QColor("purple")}, {7, QColor("orange")},
+ {8, QColor("yellow")}, {9, QColor("lightgreen")},
+ {10, QColor("cyan")}, {11, QColor("lightcyan")},
+ {12, QColor("lightblue")}, {13, QColor("pink")},
+ {14, QColor("gray")}, {15, QColor("lightgray")},
+ {16, QColor("#470000")}, {17, QColor("#472100")},
+ {18, QColor("#474700")}, {19, QColor("#324700")},
+ {20, QColor("#004700")}, {21, QColor("#00472c")},
+ {22, QColor("#004747")}, {23, QColor("#002747")},
+ {24, QColor("#000047")}, {25, QColor("#2e0047")},
+ {26, QColor("#470047")}, {27, QColor("#47002a")},
+ {28, QColor("#740000")}, {29, QColor("#743a00")},
+ {30, QColor("#747400")}, {31, QColor("#517400")},
+ {32, QColor("#007400")}, {33, QColor("#007449")},
+ {34, QColor("#007474")}, {35, QColor("#004074")},
+ {36, QColor("#000074")}, {37, QColor("#4b0074")},
+ {38, QColor("#740074")}, {39, QColor("#740045")},
+ {40, QColor("#b50000")}, {41, QColor("#b56300")},
+ {42, QColor("#b5b500")}, {43, QColor("#7db500")},
+ {44, QColor("#00b500")}, {45, QColor("#00b571")},
+ {46, QColor("#00b5b5")}, {47, QColor("#0063b5")},
+ {48, QColor("#0000b5")}, {49, QColor("#7500b5")},
+ {50, QColor("#b500b5")}, {51, QColor("#b5006b")},
+ {52, QColor("#ff0000")}, {53, QColor("#ff8c00")},
+ {54, QColor("#ffff00")}, {55, QColor("#b2ff00")},
+ {56, QColor("#00ff00")}, {57, QColor("#00ffa0")},
+ {58, QColor("#00ffff")}, {59, QColor("#008cff")},
+ {60, QColor("#0000ff")}, {61, QColor("#a500ff")},
+ {62, QColor("#ff00ff")}, {63, QColor("#ff0098")},
+ {64, QColor("#ff5959")}, {65, QColor("#ffb459")},
+ {66, QColor("#ffff71")}, {67, QColor("#cfff60")},
+ {68, QColor("#6fff6f")}, {69, QColor("#65ffc9")},
+ {70, QColor("#6dffff")}, {71, QColor("#59b4ff")},
+ {72, QColor("#5959ff")}, {73, QColor("#c459ff")},
+ {74, QColor("#ff66ff")}, {75, QColor("#ff59bc")},
+ {76, QColor("#ff9c9c")}, {77, QColor("#ffd39c")},
+ {78, QColor("#ffff9c")}, {79, QColor("#e2ff9c")},
+ {80, QColor("#9cff9c")}, {81, QColor("#9cffdb")},
+ {82, QColor("#9cffff")}, {83, QColor("#9cd3ff")},
+ {84, QColor("#9c9cff")}, {85, QColor("#dc9cff")},
+ {86, QColor("#ff9cff")}, {87, QColor("#ff94d3")},
+ {88, QColor("#000000")}, {89, QColor("#131313")},
+ {90, QColor("#282828")}, {91, QColor("#363636")},
+ {92, QColor("#4d4d4d")}, {93, QColor("#656565")},
+ {94, QColor("#818181")}, {95, QColor("#9f9f9f")},
+ {96, QColor("#bcbcbc")}, {97, QColor("#e2e2e2")},
+ {98, QColor("#ffffff")},
+};
+
+} // namespace chatterino
diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp
index c4d3fb29a..ffb64f708 100644
--- a/src/common/NetworkRequest.cpp
+++ b/src/common/NetworkRequest.cpp
@@ -99,6 +99,20 @@ NetworkRequest NetworkRequest::header(const char *headerName,
return std::move(*this);
}
+NetworkRequest NetworkRequest::headerList(const QStringList &headers) &&
+{
+ for (const QString &header : headers)
+ {
+ const QStringList thisHeader = header.trimmed().split(":");
+ if (thisHeader.size() == 2)
+ {
+ this->data->request_.setRawHeader(thisHeader[0].trimmed().toUtf8(),
+ thisHeader[1].trimmed().toUtf8());
+ }
+ }
+ return std::move(*this);
+}
+
NetworkRequest NetworkRequest::timeout(int ms) &&
{
this->data->hasTimeout_ = true;
diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp
index 3e806c3e0..509505079 100644
--- a/src/common/NetworkRequest.hpp
+++ b/src/common/NetworkRequest.hpp
@@ -3,6 +3,7 @@
#include "common/NetworkCommon.hpp"
#include "common/NetworkResult.hpp"
+#include
#include
namespace chatterino {
@@ -52,6 +53,7 @@ public:
NetworkRequest header(const char *headerName, const char *value) &&;
NetworkRequest header(const char *headerName, const QByteArray &value) &&;
NetworkRequest header(const char *headerName, const QString &value) &&;
+ NetworkRequest headerList(const QStringList &headers) &&;
NetworkRequest timeout(int ms) &&;
NetworkRequest concurrent() &&;
NetworkRequest authorizeTwitchV5(const QString &clientID,
diff --git a/src/common/SignalVector.hpp b/src/common/SignalVector.hpp
index 2995cbba4..33bd8254c 100644
--- a/src/common/SignalVector.hpp
+++ b/src/common/SignalVector.hpp
@@ -4,244 +4,171 @@
#include
#include
#include
-#include
#include
#include "debug/AssertInGuiThread.hpp"
namespace chatterino {
-template
-struct SignalVectorItemArgs {
- const TVectorItem &item;
+template
+struct SignalVectorItemEvent {
+ const T &item;
int index;
void *caller;
};
-template
-class ReadOnlySignalVector : boost::noncopyable
+template
+class SignalVector : boost::noncopyable
{
- using VecIt = typename std::vector::iterator;
-
public:
- struct Iterator
- : public std::iterator {
- Iterator(VecIt &&it, std::shared_mutex &mutex)
- : it_(std::move(it))
- , lock_(mutex)
- , mutex_(mutex)
- {
- }
+ pajlada::Signals::Signal> itemInserted;
+ pajlada::Signals::Signal> itemRemoved;
+ pajlada::Signals::NoArgSignal delayedItemsChanged;
- Iterator(const Iterator &other)
- : it_(other.it_)
- , lock_(other.mutex_)
- , mutex_(other.mutex_)
- {
- }
-
- Iterator &operator=(const Iterator &other)
- {
- this->lock_ = std::shared_lock(other.mutex_.get());
- this->mutex_ = other.mutex_;
-
- return *this;
- }
-
- TVectorItem &operator*()
- {
- return it_.operator*();
- }
-
- Iterator &operator++()
- {
- ++this->it_;
- return *this;
- }
-
- bool operator==(const Iterator &other)
- {
- return this->it_ == other.it_;
- }
-
- bool operator!=(const Iterator &other)
- {
- return this->it_ != other.it_;
- }
-
- auto operator-(const Iterator &other)
- {
- return this->it_ - other.it_;
- }
-
- private:
- VecIt it_;
- std::shared_lock lock_;
- std::reference_wrapper mutex_;
- };
-
- ReadOnlySignalVector()
+ SignalVector()
+ : readOnly_(new std::vector())
{
QObject::connect(&this->itemsChangedTimer_, &QTimer::timeout,
[this] { this->delayedItemsChanged.invoke(); });
this->itemsChangedTimer_.setInterval(100);
this->itemsChangedTimer_.setSingleShot(true);
}
- virtual ~ReadOnlySignalVector() = default;
- pajlada::Signals::Signal> itemInserted;
- pajlada::Signals::Signal> itemRemoved;
- pajlada::Signals::NoArgSignal delayedItemsChanged;
-
- Iterator begin() const
+ SignalVector(std::function &&compare)
+ : SignalVector()
{
- return Iterator(
- const_cast &>(this->vector_).begin(),
- this->mutex_);
+ itemCompare_ = std::move(compare);
}
- Iterator end() const
+ virtual bool isSorted() const
{
- return Iterator(
- const_cast &>(this->vector_).end(),
- this->mutex_);
+ return bool(this->itemCompare_);
}
- bool empty() const
+ /// A read-only version of the vector which can be used concurrently.
+ std::shared_ptr> readOnly()
{
- std::shared_lock lock(this->mutex_);
-
- return this->vector_.empty();
+ return this->readOnly_;
}
- const std::vector &getVector() const
+ /// This may only be called from the GUI thread.
+ ///
+ /// @param item
+ /// Item to be inserted.
+ /// @param proposedIndex
+ /// Index to insert at. `-1` will append at the end.
+ /// Will be ignored if the vector is sorted.
+ /// @param caller
+ /// Caller id which will be passed in the itemInserted and itemRemoved
+ /// signals.
+ int insert(const T &item, int index = -1, void *caller = nullptr)
{
assertInGuiThread();
- return this->vector_;
+ if (this->isSorted())
+ {
+ auto it = std::lower_bound(this->items_.begin(), this->items_.end(),
+ item, this->itemCompare_);
+ index = it - this->items_.begin();
+ this->items_.insert(it, item);
+ }
+ else
+ {
+ if (index == -1)
+ index = this->items_.size();
+ else
+ assert(index >= 0 && index <= this->items_.size());
+
+ this->items_.insert(this->items_.begin() + index, item);
+ }
+
+ SignalVectorItemEvent args{item, index, caller};
+ this->itemInserted.invoke(args);
+ this->itemsChanged_();
+
+ return index;
}
- std::vector cloneVector() const
+ /// This may only be called from the GUI thread.
+ ///
+ /// @param item
+ /// Item to be appended.
+ /// @param caller
+ /// Caller id which will be passed in the itemInserted and itemRemoved
+ /// signals.
+ int append(const T &item, void *caller = nullptr)
{
- std::shared_lock lock(this->mutex_);
-
- return this->vector_;
+ return this->insert(item, -1, caller);
}
- void invokeDelayedItemsChanged()
+ void removeAt(int index, void *caller = nullptr)
+ {
+ assertInGuiThread();
+ assert(index >= 0 && index < int(this->items_.size()));
+
+ T item = this->items_[index];
+ this->items_.erase(this->items_.begin() + index);
+
+ SignalVectorItemEvent args{item, index, caller};
+ this->itemRemoved.invoke(args);
+
+ this->itemsChanged_();
+ }
+
+ const std::vector &raw() const
{
assertInGuiThread();
+ return this->items_;
+ }
+
+ [[deprecated]] std::vector cloneVector()
+ {
+ return *this->readOnly();
+ }
+
+ // mirror vector functions
+ auto begin() const
+ {
+ assertInGuiThread();
+ return this->items_.begin();
+ }
+
+ auto end() const
+ {
+ assertInGuiThread();
+ return this->items_.end();
+ }
+
+ decltype(auto) operator[](size_t index)
+ {
+ assertInGuiThread();
+ return this->items[index];
+ }
+
+ auto empty()
+ {
+ assertInGuiThread();
+ return this->items_.empty();
+ }
+
+private:
+ void itemsChanged_()
+ {
+ // emit delayed event
if (!this->itemsChangedTimer_.isActive())
{
this->itemsChangedTimer_.start();
}
+
+ // update concurrent version
+ this->readOnly_ = std::make_shared>(this->items_);
}
- virtual bool isSorted() const = 0;
-
-protected:
- std::vector vector_;
+ std::vector items_;
+ std::shared_ptr> readOnly_;
QTimer itemsChangedTimer_;
- mutable std::shared_mutex mutex_;
-};
-
-template
-class BaseSignalVector : public ReadOnlySignalVector
-{
-public:
- // returns the actual index of the inserted item
- virtual int insertItem(const TVectorItem &item, int proposedIndex = -1,
- void *caller = nullptr) = 0;
-
- void removeItem(int index, void *caller = nullptr)
- {
- assertInGuiThread();
- std::unique_lock lock(this->mutex_);
-
- assert(index >= 0 && index < int(this->vector_.size()));
-
- TVectorItem item = this->vector_[index];
-
- this->vector_.erase(this->vector_.begin() + index);
- lock.unlock(); // manual unlock
-
- SignalVectorItemArgs args{item, index, caller};
- this->itemRemoved.invoke(args);
-
- this->invokeDelayedItemsChanged();
- }
-
- int appendItem(const TVectorItem &item, void *caller = nullptr)
- {
- return this->insertItem(item, -1, caller);
- }
-};
-
-template
-class UnsortedSignalVector : public BaseSignalVector
-{
-public:
- virtual int insertItem(const TVectorItem &item, int index = -1,
- void *caller = nullptr) override
- {
- assertInGuiThread();
-
- {
- std::unique_lock lock(this->mutex_);
- if (index == -1)
- {
- index = this->vector_.size();
- }
- else
- {
- assert(index >= 0 && index <= this->vector_.size());
- }
-
- this->vector_.insert(this->vector_.begin() + index, item);
- }
-
- SignalVectorItemArgs args{item, index, caller};
- this->itemInserted.invoke(args);
- this->invokeDelayedItemsChanged();
- return index;
- }
-
- virtual bool isSorted() const override
- {
- return false;
- }
-};
-
-template
-class SortedSignalVector : public BaseSignalVector
-{
-public:
- virtual int insertItem(const TVectorItem &item, int = -1,
- void *caller = nullptr) override
- {
- assertInGuiThread();
- int index = -1;
-
- {
- std::unique_lock lock(this->mutex_);
-
- auto it = std::lower_bound(this->vector_.begin(),
- this->vector_.end(), item, Compare{});
- index = it - this->vector_.begin();
- this->vector_.insert(it, item);
- }
-
- SignalVectorItemArgs args{item, index, caller};
- this->itemInserted.invoke(args);
- this->invokeDelayedItemsChanged();
- return index;
- }
-
- virtual bool isSorted() const override
- {
- return true;
- }
+ std::function itemCompare_;
};
} // namespace chatterino
diff --git a/src/common/SignalVectorModel.hpp b/src/common/SignalVectorModel.hpp
index adea78843..76bc14b22 100644
--- a/src/common/SignalVectorModel.hpp
+++ b/src/common/SignalVectorModel.hpp
@@ -25,11 +25,11 @@ public:
}
}
- void init(BaseSignalVector *vec)
+ void initialize(SignalVector *vec)
{
this->vector_ = vec;
- auto insert = [this](const SignalVectorItemArgs &args) {
+ auto insert = [this](const SignalVectorItemEvent &args) {
if (args.caller == this)
{
return;
@@ -52,9 +52,9 @@ public:
};
int i = 0;
- for (const TVectorItem &item : vec->getVector())
+ for (const TVectorItem &item : vec->raw())
{
- SignalVectorItemArgs args{item, i++, 0};
+ SignalVectorItemEvent args{item, i++, 0};
insert(args);
}
@@ -89,6 +89,12 @@ public:
this->afterInit();
}
+ SignalVectorModel *initialized(SignalVector *vec)
+ {
+ this->initialize(vec);
+ return this;
+ }
+
virtual ~SignalVectorModel()
{
for (Row &row : this->rows_)
@@ -147,12 +153,12 @@ public:
else
{
int vecRow = this->getVectorIndexFromModelIndex(row);
- this->vector_->removeItem(vecRow, this);
+ this->vector_->removeAt(vecRow, this);
assert(this->rows_[row].original);
TVectorItem item = this->getItemFromRow(
this->rows_[row].items, this->rows_[row].original.get());
- this->vector_->insertItem(item, vecRow, this);
+ this->vector_->insert(item, vecRow, this);
}
return true;
@@ -219,7 +225,7 @@ public:
void deleteRow(int row)
{
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
- this->vector_->removeItem(signalVectorRow);
+ this->vector_->removeAt(signalVectorRow);
}
bool removeRows(int row, int count, const QModelIndex &parent) override
@@ -234,11 +240,71 @@ public:
assert(row >= 0 && row < this->rows_.size());
int signalVectorRow = this->getVectorIndexFromModelIndex(row);
- this->vector_->removeItem(signalVectorRow);
+ this->vector_->removeAt(signalVectorRow);
return true;
}
+ QStringList mimeTypes() const override
+ {
+ return {"chatterino_row_id"};
+ }
+
+ QMimeData *mimeData(const QModelIndexList &list) const
+ {
+ if (list.length() == 1)
+ {
+ return nullptr;
+ }
+
+ // Check if all indices are in the same row -> single row selected
+ for (auto &&x : list)
+ {
+ if (x.row() != list.first().row())
+ return nullptr;
+ }
+
+ auto data = new QMimeData;
+ data->setData("chatterino_row_id", QByteArray::number(list[0].row()));
+ return data;
+ }
+
+ bool dropMimeData(const QMimeData *data, Qt::DropAction action, int /*row*/,
+ int /*column*/, const QModelIndex &parent) override
+ {
+ if (data->hasFormat("chatterino_row_id") &&
+ action & (Qt::DropAction::MoveAction | Qt::DropAction::CopyAction))
+ {
+ int from = data->data("chatterino_row_id").toInt();
+ int to = parent.row();
+
+ if (from < 0 || from > this->vector_->raw().size() || to < 0 ||
+ to > this->vector_->raw().size())
+ {
+ return false;
+ }
+
+ if (from != to)
+ {
+ auto item = this->vector_->raw()[from];
+ this->vector_->removeAt(from);
+ this->vector_->insert(item, to);
+ }
+
+ // We return false since we remove items ourselves.
+ return false;
+ }
+
+ return false;
+ }
+
+ Qt::DropActions supportedDropActions() const override
+ {
+ return this->vector_->isSorted()
+ ? Qt::DropActions()
+ : Qt::DropAction::CopyAction | Qt::DropAction::MoveAction;
+ }
+
protected:
virtual void afterInit()
{
@@ -326,7 +392,7 @@ protected:
private:
std::vector> headerData_;
- BaseSignalVector *vector_;
+ SignalVector *vector_;
std::vector rows_;
int columnCount_;
diff --git a/src/common/UsernameSet.cpp b/src/common/UsernameSet.cpp
index c9f9a031c..958a6fc3a 100644
--- a/src/common/UsernameSet.cpp
+++ b/src/common/UsernameSet.cpp
@@ -63,6 +63,11 @@ void UsernameSet::insertPrefix(const QString &value)
string = value;
}
+bool UsernameSet::contains(const QString &value) const
+{
+ return this->items.count(value) == 1;
+}
+
//
// Range
//
diff --git a/src/common/UsernameSet.hpp b/src/common/UsernameSet.hpp
index 9a24d239b..f43916cc4 100644
--- a/src/common/UsernameSet.hpp
+++ b/src/common/UsernameSet.hpp
@@ -76,6 +76,8 @@ public:
std::pair insert(const QString &value);
std::pair insert(QString &&value);
+ bool contains(const QString &value) const;
+
private:
void insertPrefix(const QString &string);
diff --git a/src/controllers/accounts/AccountController.cpp b/src/controllers/accounts/AccountController.cpp
index cdaa53285..2692b05a9 100644
--- a/src/controllers/accounts/AccountController.cpp
+++ b/src/controllers/accounts/AccountController.cpp
@@ -7,20 +7,20 @@
namespace chatterino {
AccountController::AccountController()
+ : accounts_(SharedPtrElementLess{})
{
this->twitch.accounts.itemInserted.connect([this](const auto &args) {
- this->accounts_.insertItem(
- std::dynamic_pointer_cast(args.item));
+ this->accounts_.insert(std::dynamic_pointer_cast(args.item));
});
this->twitch.accounts.itemRemoved.connect([this](const auto &args) {
if (args.caller != this)
{
- auto &accs = this->twitch.accounts.getVector();
+ auto &accs = this->twitch.accounts.raw();
auto it = std::find(accs.begin(), accs.end(), args.item);
assert(it != accs.end());
- this->accounts_.removeItem(it - accs.begin(), this);
+ this->accounts_.removeAt(it - accs.begin(), this);
}
});
@@ -30,10 +30,10 @@ AccountController::AccountController()
case ProviderId::Twitch: {
if (args.caller != this)
{
- auto accs = this->twitch.accounts.cloneVector();
+ auto &&accs = this->twitch.accounts;
auto it = std::find(accs.begin(), accs.end(), args.item);
assert(it != accs.end());
- this->twitch.accounts.removeItem(it - accs.begin(), this);
+ this->twitch.accounts.removeAt(it - accs.begin(), this);
}
}
break;
@@ -50,7 +50,7 @@ AccountModel *AccountController::createModel(QObject *parent)
{
AccountModel *model = new AccountModel(parent);
- model->init(&this->accounts_);
+ model->initialize(&this->accounts_);
return model;
}
diff --git a/src/controllers/accounts/AccountController.hpp b/src/controllers/accounts/AccountController.hpp
index e297b9e4d..e1ef3c70a 100644
--- a/src/controllers/accounts/AccountController.hpp
+++ b/src/controllers/accounts/AccountController.hpp
@@ -27,8 +27,7 @@ public:
TwitchAccountManager twitch;
private:
- SortedSignalVector, SharedPtrElementLess>
- accounts_;
+ SignalVector> accounts_;
};
} // namespace chatterino
diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp
index a6150cfa9..a6f4746f5 100644
--- a/src/controllers/commands/CommandController.cpp
+++ b/src/controllers/commands/CommandController.cpp
@@ -8,28 +8,29 @@
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
-#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
+#include "providers/twitch/api/Helix.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/CombinePath.hpp"
-#include "widgets/dialogs/LogsPopup.hpp"
+#include "util/Twitch.hpp"
#include "widgets/dialogs/UserInfoPopup.hpp"
#include
#include
#include
-#define TWITCH_DEFAULT_COMMANDS \
- { \
- "/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \
- "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
- "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
- "/clear", "/subscribers", "/subscribersoff", "/followers", \
- "/followersoff", "/user" \
+#define TWITCH_DEFAULT_COMMANDS \
+ { \
+ "/help", "/w", "/me", "/disconnect", "/mods", "/color", "/ban", \
+ "/unban", "/timeout", "/untimeout", "/slow", "/slowoff", \
+ "/r9kbeta", "/r9kbetaoff", "/emoteonly", "/emoteonlyoff", \
+ "/clear", "/subscribers", "/subscribersoff", "/followers", \
+ "/followersoff", "/user", "/usercard", "/follow", "/unfollow", \
+ "/ignore", "/unignore" \
}
namespace {
@@ -212,7 +213,7 @@ void CommandController::initialize(Settings &, Paths &paths)
// Update the setting when the vector of commands has been updated (most
// likely from the settings dialog)
this->items_.delayedItemsChanged.connect([this] { //
- this->commandsSetting_->setValue(this->items_.getVector());
+ this->commandsSetting_->setValue(this->items_.raw());
});
// Load commands from commands.json
@@ -222,7 +223,7 @@ void CommandController::initialize(Settings &, Paths &paths)
// of commands)
for (const auto &command : this->commandsSetting_->getValue())
{
- this->items_.appendItem(command);
+ this->items_.append(command);
}
}
@@ -234,7 +235,7 @@ void CommandController::save()
CommandModel *CommandController::createModel(QObject *parent)
{
CommandModel *model = new CommandModel(parent);
- model->init(&this->items_);
+ model->initialize(&this->items_);
return model;
}
@@ -245,8 +246,6 @@ QString CommandController::execCommand(const QString &textNoEmoji,
QString text = getApp()->emotes->emojis.replaceShortCodes(textNoEmoji);
QStringList words = text.split(' ', QString::SkipEmptyParts);
- std::lock_guard lock(this->mutex_);
-
if (words.length() == 0)
{
return text;
@@ -366,18 +365,17 @@ QString CommandController::execCommand(const QString &textNoEmoji,
return "";
}
- TwitchApi::findUserId(
- target, [user, channel, target](QString userId) {
- if (userId.isEmpty())
- {
- channel->addMessage(makeSystemMessage(
- "User " + target + " could not be followed!"));
- return;
- }
- user->followUser(userId, [channel, target]() {
+ getHelix()->getUserByName(
+ target,
+ [user, channel, target](const auto &targetUser) {
+ user->followUser(targetUser.id, [channel, target]() {
channel->addMessage(makeSystemMessage(
"You successfully followed " + target));
});
+ },
+ [channel, target] {
+ channel->addMessage(makeSystemMessage(
+ "User " + target + " could not be followed!"));
});
return "";
@@ -402,63 +400,26 @@ QString CommandController::execCommand(const QString &textNoEmoji,
return "";
}
- TwitchApi::findUserId(
- target, [user, channel, target](QString userId) {
- if (userId.isEmpty())
- {
- channel->addMessage(makeSystemMessage(
- "User " + target + " could not be followed!"));
- return;
- }
- user->unfollowUser(userId, [channel, target]() {
+ getHelix()->getUserByName(
+ target,
+ [user, channel, target](const auto &targetUser) {
+ user->unfollowUser(targetUser.id, [channel, target]() {
channel->addMessage(makeSystemMessage(
"You successfully unfollowed " + target));
});
+ },
+ [channel, target] {
+ channel->addMessage(makeSystemMessage(
+ "User " + target + " could not be followed!"));
});
return "";
}
else if (commandName == "/logs")
{
- if (words.size() < 2)
- {
- channel->addMessage(
- makeSystemMessage("Usage: /logs [user] (channel)"));
- return "";
- }
- auto app = getApp();
-
- auto logs = new LogsPopup();
- QString target = words.at(1);
-
- if (target.at(0) == '@')
- {
- target = target.mid(1);
- }
-
- logs->setTargetUserName(target);
-
- std::shared_ptr logsChannel = channel;
-
- if (words.size() == 3)
- {
- QString channelName = words.at(2);
- if (words.at(2).at(0) == '#')
- {
- channelName = channelName.mid(1);
- }
-
- logs->setChannelName(channelName);
-
- logsChannel =
- app->twitch.server->getChannelOrEmpty(channelName);
- }
-
- logs->setChannel(logsChannel);
-
- logs->getLogs();
- logs->setAttribute(Qt::WA_DeleteOnClose);
- logs->show();
+ channel->addMessage(makeSystemMessage(
+ "Online logs functionality has been removed. If you're a "
+ "moderator, you can use the /user command"));
return "";
}
else if (commandName == "/user")
@@ -478,8 +439,8 @@ QString CommandController::execCommand(const QString &textNoEmoji,
channelName.remove(0, 1);
}
}
- QDesktopServices::openUrl("https://www.twitch.tv/popout/" +
- channelName + "/viewercard/" + words[1]);
+ openTwitchUsercard(channelName, words[1]);
+
return "";
}
else if (commandName == "/usercard")
@@ -492,7 +453,6 @@ QString CommandController::execCommand(const QString &textNoEmoji,
}
auto *userPopup = new UserInfoPopup;
userPopup->setData(words[1], channel);
- userPopup->setActionOnFocusLoss(BaseWindow::Delete);
userPopup->move(QCursor::pos());
userPopup->show();
return "";
diff --git a/src/controllers/commands/CommandController.hpp b/src/controllers/commands/CommandController.hpp
index 938fe0f39..ebc57d8cb 100644
--- a/src/controllers/commands/CommandController.hpp
+++ b/src/controllers/commands/CommandController.hpp
@@ -22,7 +22,7 @@ class CommandModel;
class CommandController final : public Singleton
{
public:
- UnsortedSignalVector items_;
+ SignalVector items_;
QString execCommand(const QString &text, std::shared_ptr channel,
bool dryRun);
@@ -39,8 +39,6 @@ private:
QMap commandsMap_;
int maxSpaces_ = 0;
- std::mutex mutex_;
-
std::shared_ptr sm_;
// Because the setting manager is not initialized until the initialize
// function is called (and not in the constructor), we have to
diff --git a/src/controllers/commands/CommandModel.cpp b/src/controllers/commands/CommandModel.cpp
index 4a55907d0..d12a81c69 100644
--- a/src/controllers/commands/CommandModel.cpp
+++ b/src/controllers/commands/CommandModel.cpp
@@ -1,5 +1,7 @@
#include "CommandModel.hpp"
+#include "util/StandardItemHelper.hpp"
+
namespace chatterino {
// commandmodel
@@ -20,12 +22,8 @@ Command CommandModel::getItemFromRow(std::vector &row,
void CommandModel::getRowFromItem(const Command &item,
std::vector &row)
{
- row[0]->setData(item.name, Qt::DisplayRole);
- row[0]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
- Qt::ItemIsEditable);
- row[1]->setData(item.func, Qt::DisplayRole);
- row[1]->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
- Qt::ItemIsEditable);
+ setStringItem(row[0], item.name);
+ setStringItem(row[1], item.func);
}
} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightBlacklistModel.hpp b/src/controllers/highlights/HighlightBlacklistModel.hpp
index c3420b066..d4acc474d 100644
--- a/src/controllers/highlights/HighlightBlacklistModel.hpp
+++ b/src/controllers/highlights/HighlightBlacklistModel.hpp
@@ -11,9 +11,9 @@ class HighlightController;
class HighlightBlacklistModel : public SignalVectorModel
{
+public:
explicit HighlightBlacklistModel(QObject *parent);
-public:
enum Column {
Pattern = 0,
UseRegex = 1,
@@ -28,8 +28,6 @@ protected:
// turns a row in the model into a vector item
virtual void getRowFromItem(const HighlightBlacklistUser &item,
std::vector &row) override;
-
- friend class HighlightController;
};
} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightController.cpp b/src/controllers/highlights/HighlightController.cpp
deleted file mode 100644
index d030f77b8..000000000
--- a/src/controllers/highlights/HighlightController.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-#include "HighlightController.hpp"
-
-#include "Application.hpp"
-#include "controllers/highlights/HighlightBlacklistModel.hpp"
-#include "controllers/highlights/HighlightModel.hpp"
-#include "controllers/highlights/UserHighlightModel.hpp"
-#include "widgets/dialogs/NotificationPopup.hpp"
-
-namespace chatterino {
-
-HighlightController::HighlightController()
-{
-}
-
-void HighlightController::initialize(Settings &settings, Paths &paths)
-{
- assert(!this->initialized_);
- this->initialized_ = true;
-
- for (const HighlightPhrase &phrase : this->highlightsSetting_.getValue())
- {
- this->phrases.appendItem(phrase);
- }
-
- this->phrases.delayedItemsChanged.connect([this] { //
- this->highlightsSetting_.setValue(this->phrases.getVector());
- });
-
- for (const HighlightBlacklistUser &blacklistedUser :
- this->blacklistSetting_.getValue())
- {
- this->blacklistedUsers.appendItem(blacklistedUser);
- }
-
- this->blacklistedUsers.delayedItemsChanged.connect([this] {
- this->blacklistSetting_.setValue(this->blacklistedUsers.getVector());
- });
-
- for (const HighlightPhrase &user : this->userSetting_.getValue())
- {
- this->highlightedUsers.appendItem(user);
- }
-
- this->highlightedUsers.delayedItemsChanged.connect([this] { //
- this->userSetting_.setValue(this->highlightedUsers.getVector());
- });
-}
-
-HighlightModel *HighlightController::createModel(QObject *parent)
-{
- HighlightModel *model = new HighlightModel(parent);
- model->init(&this->phrases);
-
- return model;
-}
-
-UserHighlightModel *HighlightController::createUserModel(QObject *parent)
-{
- auto *model = new UserHighlightModel(parent);
- model->init(&this->highlightedUsers);
-
- return model;
-}
-
-bool HighlightController::isHighlightedUser(const QString &username)
-{
- const auto &userItems = this->highlightedUsers;
- for (const auto &highlightedUser : userItems)
- {
- if (highlightedUser.isMatch(username))
- {
- return true;
- }
- }
-
- return false;
-}
-
-HighlightBlacklistModel *HighlightController::createBlacklistModel(
- QObject *parent)
-{
- auto *model = new HighlightBlacklistModel(parent);
- model->init(&this->blacklistedUsers);
-
- return model;
-}
-
-bool HighlightController::blacklistContains(const QString &username)
-{
- for (const auto &blacklistedUser : this->blacklistedUsers)
- {
- if (blacklistedUser.isMatch(username))
- {
- return true;
- }
- }
-
- return false;
-}
-
-void HighlightController::addHighlight(const MessagePtr &msg)
-{
- // static NotificationPopup popup;
-
- // popup.updatePosition();
- // popup.addMessage(msg);
- // popup.show();
-}
-
-} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightController.hpp b/src/controllers/highlights/HighlightController.hpp
deleted file mode 100644
index faa4683c1..000000000
--- a/src/controllers/highlights/HighlightController.hpp
+++ /dev/null
@@ -1,52 +0,0 @@
-#pragma once
-
-#include "common/ChatterinoSetting.hpp"
-#include "common/SignalVector.hpp"
-#include "common/Singleton.hpp"
-#include "controllers/highlights/HighlightBlacklistUser.hpp"
-#include "controllers/highlights/HighlightPhrase.hpp"
-
-namespace chatterino {
-
-struct Message;
-using MessagePtr = std::shared_ptr;
-
-class Settings;
-class Paths;
-
-class UserHighlightModel;
-class HighlightModel;
-class HighlightBlacklistModel;
-
-class HighlightController final : public Singleton
-{
-public:
- HighlightController();
-
- virtual void initialize(Settings &settings, Paths &paths) override;
-
- UnsortedSignalVector phrases;
- UnsortedSignalVector blacklistedUsers;
- UnsortedSignalVector highlightedUsers;
-
- HighlightModel *createModel(QObject *parent);
- HighlightBlacklistModel *createBlacklistModel(QObject *parent);
- UserHighlightModel *createUserModel(QObject *parent);
-
- bool isHighlightedUser(const QString &username);
- bool blacklistContains(const QString &username);
-
- void addHighlight(const MessagePtr &msg);
-
-private:
- bool initialized_ = false;
-
- ChatterinoSetting> highlightsSetting_ = {
- "/highlighting/highlights"};
- ChatterinoSetting> blacklistSetting_ = {
- "/highlighting/blacklist"};
- ChatterinoSetting> userSetting_ = {
- "/highlighting/users"};
-};
-
-} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp
index 05f37bf78..12135e3be 100644
--- a/src/controllers/highlights/HighlightModel.cpp
+++ b/src/controllers/highlights/HighlightModel.cpp
@@ -2,6 +2,7 @@
#include "Application.hpp"
#include "singletons/Settings.hpp"
+#include "singletons/WindowManager.hpp"
#include "util/StandardItemHelper.hpp"
namespace chatterino {
@@ -63,10 +64,10 @@ void HighlightModel::afterInit()
usernameRow[Column::CaseSensitive]->setFlags(0);
QUrl selfSound = QUrl(getSettings()->selfHighlightSoundUrl.getValue());
- setFilePathItem(usernameRow[Column::SoundPath], selfSound);
+ setFilePathItem(usernameRow[Column::SoundPath], selfSound, false);
auto selfColor = ColorProvider::instance().color(ColorType::SelfHighlight);
- setColorItem(usernameRow[Column::Color], *selfColor);
+ setColorItem(usernameRow[Column::Color], *selfColor, false);
this->insertCustomRow(usernameRow, 0);
@@ -86,12 +87,13 @@ void HighlightModel::afterInit()
QUrl whisperSound =
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
- setFilePathItem(whisperRow[Column::SoundPath], whisperSound);
+ setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
- auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
- setColorItem(whisperRow[Column::Color], *whisperColor);
+ // auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
+ // setColorItem(whisperRow[Column::Color], *whisperColor, false);
+ whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
- this->insertCustomRow(whisperRow, 1);
+ this->insertCustomRow(whisperRow, WHISPER_ROW);
// Highlight settings for subscription messages
std::vector subRow = this->createRow();
@@ -107,12 +109,39 @@ void HighlightModel::afterInit()
subRow[Column::CaseSensitive]->setFlags(0);
QUrl subSound = QUrl(getSettings()->subHighlightSoundUrl.getValue());
- setFilePathItem(subRow[Column::SoundPath], subSound);
+ setFilePathItem(subRow[Column::SoundPath], subSound, false);
auto subColor = ColorProvider::instance().color(ColorType::Subscription);
- setColorItem(subRow[Column::Color], *subColor);
+ setColorItem(subRow[Column::Color], *subColor, false);
this->insertCustomRow(subRow, 2);
+
+ // Highlight settings for redeemed highlight messages
+ std::vector redeemedRow = this->createRow();
+ setBoolItem(redeemedRow[Column::Pattern],
+ getSettings()->enableRedeemedHighlight.getValue(), true, false);
+ redeemedRow[Column::Pattern]->setData(
+ "Highlights redeemed with Channel Points", Qt::DisplayRole);
+ // setBoolItem(redeemedRow[Column::FlashTaskbar],
+ // getSettings()->enableRedeemedHighlightTaskbar.getValue(), true,
+ // false);
+ // setBoolItem(redeemedRow[Column::PlaySound],
+ // getSettings()->enableRedeemedHighlightSound.getValue(), true,
+ // false);
+ redeemedRow[Column::FlashTaskbar]->setFlags(0);
+ redeemedRow[Column::PlaySound]->setFlags(0);
+ redeemedRow[Column::UseRegex]->setFlags(0);
+ redeemedRow[Column::CaseSensitive]->setFlags(0);
+
+ QUrl RedeemedSound =
+ QUrl(getSettings()->redeemedHighlightSoundUrl.getValue());
+ setFilePathItem(redeemedRow[Column::SoundPath], RedeemedSound, false);
+
+ auto RedeemedColor =
+ ColorProvider::instance().color(ColorType::RedeemedHighlight);
+ setColorItem(redeemedRow[Column::Color], *RedeemedColor, false);
+
+ this->insertCustomRow(redeemedRow, 3);
}
void HighlightModel::customRowSetData(const std::vector &row,
@@ -128,7 +157,7 @@ void HighlightModel::customRowSetData(const std::vector &row,
{
getSettings()->enableSelfHighlight.setValue(value.toBool());
}
- else if (rowIndex == 1)
+ else if (rowIndex == WHISPER_ROW)
{
getSettings()->enableWhisperHighlight.setValue(
value.toBool());
@@ -137,6 +166,11 @@ void HighlightModel::customRowSetData(const std::vector &row,
{
getSettings()->enableSubHighlight.setValue(value.toBool());
}
+ else if (rowIndex == 3)
+ {
+ getSettings()->enableRedeemedHighlight.setValue(
+ value.toBool());
+ }
}
}
break;
@@ -148,7 +182,7 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->enableSelfHighlightTaskbar.setValue(
value.toBool());
}
- else if (rowIndex == 1)
+ else if (rowIndex == WHISPER_ROW)
{
getSettings()->enableWhisperHighlightTaskbar.setValue(
value.toBool());
@@ -158,6 +192,11 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->enableSubHighlightTaskbar.setValue(
value.toBool());
}
+ else if (rowIndex == 3)
+ {
+ // getSettings()->enableRedeemedHighlightTaskbar.setValue(
+ // value.toBool());
+ }
}
}
break;
@@ -169,7 +208,7 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->enableSelfHighlightSound.setValue(
value.toBool());
}
- else if (rowIndex == 1)
+ else if (rowIndex == WHISPER_ROW)
{
getSettings()->enableWhisperHighlightSound.setValue(
value.toBool());
@@ -179,6 +218,11 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->enableSubHighlightSound.setValue(
value.toBool());
}
+ else if (rowIndex == 3)
+ {
+ // getSettings()->enableRedeemedHighlightSound.setValue(
+ // value.toBool());
+ }
}
}
break;
@@ -199,7 +243,7 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->selfHighlightSoundUrl.setValue(
value.toString());
}
- else if (rowIndex == 1)
+ else if (rowIndex == WHISPER_ROW)
{
getSettings()->whisperHighlightSoundUrl.setValue(
value.toString());
@@ -209,6 +253,11 @@ void HighlightModel::customRowSetData(const std::vector &row,
getSettings()->subHighlightSoundUrl.setValue(
value.toString());
}
+ else if (rowIndex == 3)
+ {
+ getSettings()->redeemedHighlightSoundUrl.setValue(
+ value.toString());
+ }
}
}
break;
@@ -221,18 +270,27 @@ void HighlightModel::customRowSetData(const std::vector &row,
{
getSettings()->selfHighlightColor.setValue(colorName);
}
- else if (rowIndex == 1)
- {
- getSettings()->whisperHighlightColor.setValue(colorName);
- }
+ // else if (rowIndex == WHISPER_ROW)
+ // {
+ // getSettings()->whisperHighlightColor.setValue(colorName);
+ // }
else if (rowIndex == 2)
{
getSettings()->subHighlightColor.setValue(colorName);
}
+ else if (rowIndex == 3)
+ {
+ getSettings()->redeemedHighlightColor.setValue(colorName);
+ const_cast(ColorProvider::instance())
+ .updateColor(ColorType::RedeemedHighlight,
+ QColor(colorName));
+ }
}
}
break;
}
+
+ getApp()->windows->forceLayoutChannelViews();
}
} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp
index 13b85bca1..92acd2e0a 100644
--- a/src/controllers/highlights/HighlightModel.hpp
+++ b/src/controllers/highlights/HighlightModel.hpp
@@ -7,13 +7,11 @@
namespace chatterino {
-class HighlightController;
-
class HighlightModel : public SignalVectorModel
{
+public:
explicit HighlightModel(QObject *parent);
-public:
// Used here, in HighlightingPage and in UserHighlightModel
enum Column {
Pattern = 0,
@@ -25,6 +23,8 @@ public:
Color = 6
};
+ constexpr static int WHISPER_ROW = 1;
+
protected:
// turn a vector item into a model row
virtual HighlightPhrase getItemFromRow(
@@ -40,8 +40,6 @@ protected:
virtual void customRowSetData(const std::vector &row,
int column, const QVariant &value, int role,
int rowIndex) override;
-
- friend class HighlightController;
};
} // namespace chatterino
diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp
index 40942df3d..5ffd5b360 100644
--- a/src/controllers/highlights/HighlightPhrase.cpp
+++ b/src/controllers/highlights/HighlightPhrase.cpp
@@ -2,6 +2,11 @@
namespace chatterino {
+QColor HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR = QColor(127, 63, 73, 127);
+QColor HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR =
+ QColor(28, 126, 141, 90);
+QColor HighlightPhrase::FALLBACK_SUB_COLOR = QColor(196, 102, 255, 100);
+
bool HighlightPhrase::operator==(const HighlightPhrase &other) const
{
return std::tie(this->pattern_, this->hasSound_, this->hasAlert_,
diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp
index 72849bb66..268ae354c 100644
--- a/src/controllers/highlights/HighlightPhrase.hpp
+++ b/src/controllers/highlights/HighlightPhrase.hpp
@@ -70,6 +70,14 @@ public:
const QUrl &getSoundUrl() const;
const std::shared_ptr getColor() const;
+ /*
+ * XXX: Use the constexpr constructor here once we are building with
+ * Qt>=5.13.
+ */
+ static QColor FALLBACK_HIGHLIGHT_COLOR;
+ static QColor FALLBACK_REDEEMED_HIGHLIGHT_COLOR;
+ static QColor FALLBACK_SUB_COLOR;
+
private:
QString pattern_;
bool hasAlert_;
@@ -132,6 +140,8 @@ struct Deserialize {
chatterino::rj::getSafe(value, "color", encodedColor);
auto _color = QColor(encodedColor);
+ if (!_color.isValid())
+ _color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR;
return chatterino::HighlightPhrase(_pattern, _hasAlert, _hasSound,
_isRegex, _isCaseSensitive,
diff --git a/src/controllers/highlights/UserHighlightModel.hpp b/src/controllers/highlights/UserHighlightModel.hpp
index dcc42a950..de4332433 100644
--- a/src/controllers/highlights/UserHighlightModel.hpp
+++ b/src/controllers/highlights/UserHighlightModel.hpp
@@ -11,6 +11,7 @@ class HighlightController;
class UserHighlightModel : public SignalVectorModel
{
+public:
explicit UserHighlightModel(QObject *parent);
protected:
@@ -21,8 +22,6 @@ protected:
virtual void getRowFromItem(const HighlightPhrase &item,
std::vector &row) override;
-
- friend class HighlightController;
};
} // namespace chatterino
diff --git a/src/controllers/ignores/IgnoreController.cpp b/src/controllers/ignores/IgnoreController.cpp
deleted file mode 100644
index 5548ee5d4..000000000
--- a/src/controllers/ignores/IgnoreController.cpp
+++ /dev/null
@@ -1,33 +0,0 @@
-#include "controllers/ignores/IgnoreController.hpp"
-
-#include "Application.hpp"
-#include "controllers/ignores/IgnoreModel.hpp"
-
-#include
-
-namespace chatterino {
-
-void IgnoreController::initialize(Settings &, Paths &)
-{
- assert(!this->initialized_);
- this->initialized_ = true;
-
- for (const IgnorePhrase &phrase : this->ignoresSetting_.getValue())
- {
- this->phrases.appendItem(phrase);
- }
-
- this->phrases.delayedItemsChanged.connect([this] { //
- this->ignoresSetting_.setValue(this->phrases.getVector());
- });
-}
-
-IgnoreModel *IgnoreController::createModel(QObject *parent)
-{
- IgnoreModel *model = new IgnoreModel(parent);
- model->init(&this->phrases);
-
- return model;
-}
-
-} // namespace chatterino
diff --git a/src/controllers/ignores/IgnoreController.hpp b/src/controllers/ignores/IgnoreController.hpp
index 1279f4a88..fed12f12c 100644
--- a/src/controllers/ignores/IgnoreController.hpp
+++ b/src/controllers/ignores/IgnoreController.hpp
@@ -1,33 +1,7 @@
#pragma once
-#include "common/ChatterinoSetting.hpp"
-#include "common/SignalVector.hpp"
-#include "common/Singleton.hpp"
-#include "controllers/ignores/IgnorePhrase.hpp"
-
namespace chatterino {
-class Settings;
-class Paths;
-
-class IgnoreModel;
-
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
-class IgnoreController final : public Singleton
-{
-public:
- virtual void initialize(Settings &settings, Paths &paths) override;
-
- UnsortedSignalVector phrases;
-
- IgnoreModel *createModel(QObject *parent);
-
-private:
- bool initialized_ = false;
-
- ChatterinoSetting> ignoresSetting_ = {
- "/ignore/phrases"};
-};
-
} // namespace chatterino
diff --git a/src/controllers/ignores/IgnoreModel.hpp b/src/controllers/ignores/IgnoreModel.hpp
index 1b9a5099e..473b61b93 100644
--- a/src/controllers/ignores/IgnoreModel.hpp
+++ b/src/controllers/ignores/IgnoreModel.hpp
@@ -7,10 +7,9 @@
namespace chatterino {
-class IgnoreController;
-
class IgnoreModel : public SignalVectorModel
{
+public:
explicit IgnoreModel(QObject *parent);
protected:
@@ -21,8 +20,6 @@ protected:
// turns a row in the model into a vector item
virtual void getRowFromItem(const IgnorePhrase &item,
std::vector &row) override;
-
- friend class IgnoreController;
};
} // namespace chatterino
diff --git a/src/controllers/moderationactions/ModerationAction.cpp b/src/controllers/moderationactions/ModerationAction.cpp
index dce01904c..d94cb234d 100644
--- a/src/controllers/moderationactions/ModerationAction.cpp
+++ b/src/controllers/moderationactions/ModerationAction.cpp
@@ -71,11 +71,11 @@ ModerationAction::ModerationAction(const QString &action)
}
else if (action.startsWith("/ban "))
{
- this->image_ = Image::fromPixmap(getResources().buttons.ban);
+ this->imageToLoad_ = 1;
}
else if (action.startsWith("/delete "))
{
- this->image_ = Image::fromPixmap(getResources().buttons.trashCan);
+ this->imageToLoad_ = 2;
}
else
{
@@ -100,6 +100,16 @@ bool ModerationAction::isImage() const
const boost::optional &ModerationAction::getImage() const
{
+ assertInGuiThread();
+
+ if (this->imageToLoad_ != 0)
+ {
+ if (this->imageToLoad_ == 1)
+ this->image_ = Image::fromPixmap(getResources().buttons.ban);
+ else if (this->imageToLoad_ == 2)
+ this->image_ = Image::fromPixmap(getResources().buttons.trashCan);
+ }
+
return this->image_;
}
diff --git a/src/controllers/moderationactions/ModerationAction.hpp b/src/controllers/moderationactions/ModerationAction.hpp
index eed7579a5..3b4532d91 100644
--- a/src/controllers/moderationactions/ModerationAction.hpp
+++ b/src/controllers/moderationactions/ModerationAction.hpp
@@ -25,10 +25,11 @@ public:
const QString &getAction() const;
private:
- boost::optional image_;
+ mutable boost::optional image_;
QString line1_;
QString line2_;
QString action_;
+ int imageToLoad_{};
};
} // namespace chatterino
diff --git a/src/controllers/moderationactions/ModerationActionModel.hpp b/src/controllers/moderationactions/ModerationActionModel.hpp
index e9f372361..e13b3b27b 100644
--- a/src/controllers/moderationactions/ModerationActionModel.hpp
+++ b/src/controllers/moderationactions/ModerationActionModel.hpp
@@ -7,8 +7,6 @@
namespace chatterino {
-class ModerationActions;
-
class ModerationActionModel : public SignalVectorModel
{
public:
@@ -25,8 +23,6 @@ protected:
std::vector &row) override;
friend class HighlightController;
-
- friend class ModerationActions;
};
} // namespace chatterino
diff --git a/src/controllers/moderationactions/ModerationActions.cpp b/src/controllers/moderationactions/ModerationActions.cpp
deleted file mode 100644
index 0104c7de1..000000000
--- a/src/controllers/moderationactions/ModerationActions.cpp
+++ /dev/null
@@ -1,42 +0,0 @@
-#include "ModerationActions.hpp"
-
-#include "Application.hpp"
-#include "controllers/moderationactions/ModerationActionModel.hpp"
-#include "singletons/Settings.hpp"
-
-#include
-
-namespace chatterino {
-
-ModerationActions::ModerationActions()
-{
-}
-
-void ModerationActions::initialize(Settings &settings, Paths &paths)
-{
- assert(!this->initialized_);
- this->initialized_ = true;
-
- this->setting_ =
- std::make_unique>>(
- "/moderation/actions");
-
- for (auto &val : this->setting_->getValue())
- {
- this->items.insertItem(val);
- }
-
- this->items.delayedItemsChanged.connect([this] { //
- this->setting_->setValue(this->items.getVector());
- });
-}
-
-ModerationActionModel *ModerationActions::createModel(QObject *parent)
-{
- ModerationActionModel *model = new ModerationActionModel(parent);
- model->init(&this->items);
-
- return model;
-}
-
-} // namespace chatterino
diff --git a/src/controllers/moderationactions/ModerationActions.hpp b/src/controllers/moderationactions/ModerationActions.hpp
deleted file mode 100644
index e1bf1b802..000000000
--- a/src/controllers/moderationactions/ModerationActions.hpp
+++ /dev/null
@@ -1,32 +0,0 @@
-#pragma once
-
-#include "common/Singleton.hpp"
-
-#include "common/ChatterinoSetting.hpp"
-#include "common/SignalVector.hpp"
-#include "controllers/moderationactions/ModerationAction.hpp"
-
-namespace chatterino {
-
-class Settings;
-class Paths;
-
-class ModerationActionModel;
-
-class ModerationActions final : public Singleton
-{
-public:
- ModerationActions();
-
- virtual void initialize(Settings &settings, Paths &paths) override;
-
- UnsortedSignalVector items;
-
- ModerationActionModel *createModel(QObject *parent);
-
-private:
- std::unique_ptr>> setting_;
- bool initialized_ = false;
-};
-
-} // namespace chatterino
diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp
index b9777c802..2578558df 100644
--- a/src/controllers/notifications/NotificationController.cpp
+++ b/src/controllers/notifications/NotificationController.cpp
@@ -4,8 +4,8 @@
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "controllers/notifications/NotificationModel.hpp"
-#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
+#include "providers/twitch/api/Helix.hpp"
#include "singletons/Toasts.hpp"
#include "singletons/WindowManager.hpp"
#include "widgets/Window.hpp"
@@ -26,12 +26,11 @@ void NotificationController::initialize(Settings &settings, Paths &paths)
this->initialized_ = true;
for (const QString &channelName : this->twitchSetting_.getValue())
{
- this->channelMap[Platform::Twitch].appendItem(channelName);
+ this->channelMap[Platform::Twitch].append(channelName);
}
this->channelMap[Platform::Twitch].delayedItemsChanged.connect([this] { //
- this->twitchSetting_.setValue(
- this->channelMap[Platform::Twitch].getVector());
+ this->twitchSetting_.setValue(this->channelMap[Platform::Twitch].raw());
});
/*
for (const QString &channelName : this->mixerSetting_.getValue()) {
@@ -81,18 +80,18 @@ bool NotificationController::isChannelNotified(const QString &channelName,
void NotificationController::addChannelNotification(const QString &channelName,
Platform p)
{
- channelMap[p].appendItem(channelName);
+ channelMap[p].append(channelName);
}
void NotificationController::removeChannelNotification(
const QString &channelName, Platform p)
{
- for (std::vector::size_type i = 0;
- i != channelMap[p].getVector().size(); i++)
+ for (std::vector::size_type i = 0; i != channelMap[p].raw().size();
+ i++)
{
- if (channelMap[p].getVector()[i].toLower() == channelName.toLower())
+ if (channelMap[p].raw()[i].toLower() == channelName.toLower())
{
- channelMap[p].removeItem(i);
+ channelMap[p].removeAt(i);
i--;
}
}
@@ -121,21 +120,21 @@ NotificationModel *NotificationController::createModel(QObject *parent,
Platform p)
{
NotificationModel *model = new NotificationModel(parent);
- model->init(&this->channelMap[p]);
+ model->initialize(&this->channelMap[p]);
return model;
}
void NotificationController::fetchFakeChannels()
{
for (std::vector::size_type i = 0;
- i != channelMap[Platform::Twitch].getVector().size(); i++)
+ i != channelMap[Platform::Twitch].raw().size(); i++)
{
auto chan = getApp()->twitch.server->getChannelOrEmpty(
- channelMap[Platform::Twitch].getVector()[i]);
+ channelMap[Platform::Twitch].raw()[i]);
if (chan->isEmpty())
{
getFakeTwitchChannelLiveStatus(
- channelMap[Platform::Twitch].getVector()[i]);
+ channelMap[Platform::Twitch].raw()[i]);
}
}
}
@@ -143,68 +142,52 @@ void NotificationController::fetchFakeChannels()
void NotificationController::getFakeTwitchChannelLiveStatus(
const QString &channelName)
{
- TwitchApi::findUserId(channelName, [channelName, this](QString roomID) {
- if (roomID.isEmpty())
- {
+ getHelix()->getStreamByName(
+ channelName,
+ [channelName, this](bool live, const auto &stream) {
+ qDebug() << "[TwitchChannel" << channelName
+ << "] Refreshing live status";
+
+ if (!live)
+ {
+ // Stream is offline
+ this->removeFakeChannel(channelName);
+ return;
+ }
+
+ // Stream is online
+ auto i = std::find(fakeTwitchChannels.begin(),
+ fakeTwitchChannels.end(), channelName);
+
+ if (i != fakeTwitchChannels.end())
+ {
+ // We have already pushed the live state of this stream
+ // Could not find stream in fake twitch channels!
+ return;
+ }
+
+ if (Toasts::isEnabled())
+ {
+ getApp()->toasts->sendChannelNotification(channelName,
+ Platform::Twitch);
+ }
+ if (getSettings()->notificationPlaySound)
+ {
+ getApp()->notifications->playSound();
+ }
+ if (getSettings()->notificationFlashTaskbar)
+ {
+ getApp()->windows->sendAlert();
+ }
+
+ // Indicate that we have pushed notifications for this stream
+ fakeTwitchChannels.push_back(channelName);
+ },
+ [channelName, this] {
qDebug() << "[TwitchChannel" << channelName
<< "] Refreshing live status (Missing ID)";
- removeFakeChannel(channelName);
- return;
- }
- qDebug() << "[TwitchChannel" << channelName
- << "] Refreshing live status";
-
- QString url("https://api.twitch.tv/kraken/streams/" + roomID);
- NetworkRequest::twitchRequest(url)
- .onSuccess([this, channelName](auto result) -> Outcome {
- rapidjson::Document document = result.parseRapidJson();
- if (!document.IsObject())
- {
- qDebug() << "[TwitchChannel:refreshLiveStatus] root is not "
- "an object";
- return Failure;
- }
-
- if (!document.HasMember("stream"))
- {
- qDebug() << "[TwitchChannel:refreshLiveStatus] Missing "
- "stream in root";
- return Failure;
- }
-
- const auto &stream = document["stream"];
-
- if (!stream.IsObject())
- {
- // Stream is offline (stream is most likely null)
- // removeFakeChannel(channelName);
- return Failure;
- }
- // Stream is live
- auto i = std::find(fakeTwitchChannels.begin(),
- fakeTwitchChannels.end(), channelName);
-
- if (!(i != fakeTwitchChannels.end()))
- {
- fakeTwitchChannels.push_back(channelName);
- if (Toasts::isEnabled())
- {
- getApp()->toasts->sendChannelNotification(
- channelName, Platform::Twitch);
- }
- if (getSettings()->notificationPlaySound)
- {
- getApp()->notifications->playSound();
- }
- if (getSettings()->notificationFlashTaskbar)
- {
- getApp()->windows->sendAlert();
- }
- }
- return Success;
- })
- .execute();
- });
+ this->removeFakeChannel(channelName);
+ });
}
void NotificationController::removeFakeChannel(const QString channelName)
diff --git a/src/controllers/notifications/NotificationController.hpp b/src/controllers/notifications/NotificationController.hpp
index 228348add..e8d0c4ef7 100644
--- a/src/controllers/notifications/NotificationController.hpp
+++ b/src/controllers/notifications/NotificationController.hpp
@@ -30,9 +30,9 @@ public:
void playSound();
- UnsortedSignalVector getVector(Platform p);
+ SignalVector getVector(Platform p);
- std::map> channelMap;
+ std::map> channelMap;
NotificationModel *createModel(QObject *parent, Platform p);
@@ -43,6 +43,7 @@ private:
void removeFakeChannel(const QString channelName);
void getFakeTwitchChannelLiveStatus(const QString &channelName);
+ // fakeTwitchChannels is a list of streams who are live that we have already sent out a notification for
std::vector fakeTwitchChannels;
QTimer *liveStatusTimer_;
diff --git a/src/controllers/pings/MutedChannelModel.cpp b/src/controllers/pings/MutedChannelModel.cpp
new file mode 100644
index 000000000..fc1473506
--- /dev/null
+++ b/src/controllers/pings/MutedChannelModel.cpp
@@ -0,0 +1,28 @@
+#include "MutedChannelModel.hpp"
+
+#include "Application.hpp"
+#include "singletons/Settings.hpp"
+#include "util/StandardItemHelper.hpp"
+
+namespace chatterino {
+
+MutedChannelModel::MutedChannelModel(QObject *parent)
+ : SignalVectorModel(1, parent)
+{
+}
+
+// turn a vector item into a model row
+QString MutedChannelModel::getItemFromRow(std::vector &row,
+ const QString &original)
+{
+ return QString(row[0]->data(Qt::DisplayRole).toString());
+}
+
+// turn a model
+void MutedChannelModel::getRowFromItem(const QString &item,
+ std::vector &row)
+{
+ setStringItem(row[0], item);
+}
+
+} // namespace chatterino
diff --git a/src/controllers/pings/PingModel.hpp b/src/controllers/pings/MutedChannelModel.hpp
similarity index 79%
rename from src/controllers/pings/PingModel.hpp
rename to src/controllers/pings/MutedChannelModel.hpp
index 137be5e0c..c53b3177c 100644
--- a/src/controllers/pings/PingModel.hpp
+++ b/src/controllers/pings/MutedChannelModel.hpp
@@ -7,11 +7,11 @@
namespace chatterino {
-class PingController;
+class MutedChannelController;
-class PingModel : public SignalVectorModel
+class MutedChannelModel : public SignalVectorModel
{
- explicit PingModel(QObject *parent);
+ explicit MutedChannelModel(QObject *parent);
protected:
// turn a vector item into a model row
@@ -21,8 +21,6 @@ protected:
// turns a row in the model into a vector item
virtual void getRowFromItem(const QString &item,
std::vector &row) override;
-
- friend class PingController;
};
} // namespace chatterino
diff --git a/src/controllers/pings/PingController.cpp b/src/controllers/pings/PingController.cpp
deleted file mode 100644
index b61d6e795..000000000
--- a/src/controllers/pings/PingController.cpp
+++ /dev/null
@@ -1,70 +0,0 @@
-#include "controllers/pings/PingController.hpp"
-#include "controllers/pings/PingModel.hpp"
-
-namespace chatterino {
-
-void PingController::initialize(Settings &settings, Paths &paths)
-{
- this->initialized_ = true;
- for (const QString &channelName : this->pingSetting_.getValue())
- {
- this->channelVector.appendItem(channelName);
- }
-
- this->channelVector.delayedItemsChanged.connect([this] { //
- this->pingSetting_.setValue(this->channelVector.getVector());
- });
-}
-
-PingModel *PingController::createModel(QObject *parent)
-{
- PingModel *model = new PingModel(parent);
- model->init(&this->channelVector);
- return model;
-}
-
-bool PingController::isMuted(const QString &channelName)
-{
- for (const auto &channel : this->channelVector)
- {
- if (channelName.toLower() == channel.toLower())
- {
- return true;
- }
- }
- return false;
-}
-
-void PingController::muteChannel(const QString &channelName)
-{
- channelVector.appendItem(channelName);
-}
-
-void PingController::unmuteChannel(const QString &channelName)
-{
- for (std::vector::size_type i = 0;
- i != channelVector.getVector().size(); i++)
- {
- if (channelVector.getVector()[i].toLower() == channelName.toLower())
- {
- channelVector.removeItem(i);
- i--;
- }
- }
-}
-
-bool PingController::toggleMuteChannel(const QString &channelName)
-{
- if (this->isMuted(channelName))
- {
- unmuteChannel(channelName);
- return false;
- }
- else
- {
- muteChannel(channelName);
- return true;
- }
-}
-
-} // namespace chatterino
diff --git a/src/controllers/pings/PingController.hpp b/src/controllers/pings/PingController.hpp
deleted file mode 100644
index 80805e856..000000000
--- a/src/controllers/pings/PingController.hpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#pragma once
-
-#include
-
-#include "common/SignalVector.hpp"
-#include "common/Singleton.hpp"
-#include "singletons/Settings.hpp"
-
-namespace chatterino {
-
-class Settings;
-class Paths;
-
-class PingModel;
-
-class PingController final : public Singleton, private QObject
-{
-public:
- virtual void initialize(Settings &settings, Paths &paths) override;
-
- bool isMuted(const QString &channelName);
- void muteChannel(const QString &channelName);
- void unmuteChannel(const QString &channelName);
- bool toggleMuteChannel(const QString &channelName);
-
- PingModel *createModel(QObject *parent);
-
-private:
- bool initialized_ = false;
-
- UnsortedSignalVector channelVector;
-
- ChatterinoSetting> pingSetting_ = {"/pings/muted"};
-};
-
-} // namespace chatterino
diff --git a/src/controllers/pings/PingModel.cpp b/src/controllers/pings/PingModel.cpp
deleted file mode 100644
index 28098a209..000000000
--- a/src/controllers/pings/PingModel.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-#include "PingModel.hpp"
-
-#include "Application.hpp"
-#include "singletons/Settings.hpp"
-#include "util/StandardItemHelper.hpp"
-
-namespace chatterino {
-
-PingModel::PingModel(QObject *parent)
- : SignalVectorModel(1, parent)
-{
-}
-
-// turn a vector item into a model row
-QString PingModel::getItemFromRow(std::vector &row,
- const QString &original)
-{
- return QString(row[0]->data(Qt::DisplayRole).toString());
-}
-
-// turn a model
-void PingModel::getRowFromItem(const QString &item,
- std::vector &row)
-{
- setStringItem(row[0], item);
-}
-
-} // namespace chatterino
diff --git a/src/controllers/taggedusers/TaggedUsersController.cpp b/src/controllers/taggedusers/TaggedUsersController.cpp
deleted file mode 100644
index 2aa82a6c3..000000000
--- a/src/controllers/taggedusers/TaggedUsersController.cpp
+++ /dev/null
@@ -1,19 +0,0 @@
-#include "TaggedUsersController.hpp"
-
-#include "controllers/taggedusers/TaggedUsersModel.hpp"
-
-namespace chatterino {
-
-TaggedUsersController::TaggedUsersController()
-{
-}
-
-TaggedUsersModel *TaggedUsersController::createModel(QObject *parent)
-{
- TaggedUsersModel *model = new TaggedUsersModel(parent);
- model->init(&this->users);
-
- return model;
-}
-
-} // namespace chatterino
diff --git a/src/controllers/taggedusers/TaggedUsersController.hpp b/src/controllers/taggedusers/TaggedUsersController.hpp
deleted file mode 100644
index db9ff2d14..000000000
--- a/src/controllers/taggedusers/TaggedUsersController.hpp
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma once
-
-#include "common/Singleton.hpp"
-
-#include "common/SignalVector.hpp"
-#include "controllers/taggedusers/TaggedUser.hpp"
-
-namespace chatterino {
-
-class TaggedUsersModel;
-
-class TaggedUsersController final : public Singleton
-{
-public:
- TaggedUsersController();
-
- SortedSignalVector> users;
-
- TaggedUsersModel *createModel(QObject *parent = nullptr);
-};
-
-} // namespace chatterino
diff --git a/src/main.cpp b/src/main.cpp
index 463465894..67a05b7b9 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -9,6 +9,8 @@
#include "common/Args.hpp"
#include "common/Modes.hpp"
#include "common/Version.hpp"
+#include "providers/twitch/api/Helix.hpp"
+#include "providers/twitch/api/Kraken.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
#include "util/IncognitoBrowser.hpp"
@@ -19,6 +21,10 @@ int main(int argc, char **argv)
{
QApplication a(argc, argv);
+ QCoreApplication::setApplicationName("chatterino");
+ QCoreApplication::setApplicationVersion(CHATTERINO_VERSION);
+ QCoreApplication::setOrganizationDomain("https://www.chatterino.com");
+
// convert char** to QStringList
auto args = QStringList();
std::transform(argv + 1, argv + argc, std::back_inserter(args),
@@ -32,10 +38,19 @@ int main(int argc, char **argv)
}
else if (getArgs().printVersion)
{
- qInfo().noquote() << Version::instance().fullVersion();
+ auto version = Version::instance();
+ qInfo().noquote() << QString("%1 (commit %2%3)")
+ .arg(version.fullVersion())
+ .arg(version.commitHash())
+ .arg(Modes::instance().isNightly
+ ? ", " + version.dateOfBuild()
+ : "");
}
else
{
+ Helix::initialize();
+ Kraken::initialize();
+
Paths *paths{};
try
diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp
index b42223012..37a8a644b 100644
--- a/src/messages/Image.cpp
+++ b/src/messages/Image.cpp
@@ -41,6 +41,22 @@ namespace detail {
getApp()->emotes->gifTimer.signal.connect(
[this] { this->advance(); });
}
+
+ auto totalLength = std::accumulate(
+ this->items_.begin(), this->items_.end(), 0UL,
+ [](auto init, auto &&frame) { return init + frame.duration; });
+
+ if (totalLength == 0)
+ {
+ this->durationOffset_ = 0;
+ }
+ else
+ {
+ this->durationOffset_ = std::min(
+ int(getApp()->emotes->gifTimer.position() % totalLength),
+ 60000);
+ }
+ this->processOffset();
}
Frames::~Frames()
@@ -58,17 +74,21 @@ namespace detail {
void Frames::advance()
{
- this->durationOffset_ += GIF_FRAME_LENGTH;
+ this->durationOffset_ += gifFrameLength;
+ this->processOffset();
+ }
+
+ void Frames::processOffset()
+ {
+ if (this->items_.isEmpty())
+ {
+ return;
+ }
while (true)
{
this->index_ %= this->items_.size();
- // TODO: Figure out what this was supposed to achieve
- // if (this->index_ >= this->items_.size()) {
- // this->index_ = this->index_;
- // }
-
if (this->durationOffset_ > this->items_[this->index_].duration)
{
this->durationOffset_ -= this->items_[this->index_].duration;
@@ -219,10 +239,6 @@ ImagePtr Image::fromUrl(const Url &url, qreal scale)
{
cache[url] = shared = ImagePtr(new Image(url, scale));
}
- else
- {
- // qDebug() << "same image created multiple times:" << url.string;
- }
return shared;
}
diff --git a/src/messages/Image.hpp b/src/messages/Image.hpp
index f114d1f0f..193ba5787 100644
--- a/src/messages/Image.hpp
+++ b/src/messages/Image.hpp
@@ -35,6 +35,7 @@ namespace detail {
boost::optional first() const;
private:
+ void processOffset();
QVector> items_;
int index_{0};
int durationOffset_{0};
diff --git a/src/messages/LimitedQueue.hpp b/src/messages/LimitedQueue.hpp
index 61e8db56f..f206704f5 100644
--- a/src/messages/LimitedQueue.hpp
+++ b/src/messages/LimitedQueue.hpp
@@ -125,8 +125,6 @@ public:
newChunks->at(0) = newFirstChunk;
this->chunks_ = newChunks;
- // qDebug() << acceptedItems.size();
- // qDebug() << this->chunks->at(0)->size();
if (this->chunks_->size() == 1)
{
@@ -225,8 +223,13 @@ public:
this->firstChunkOffset_, this->lastChunkEnd_);
}
+ bool empty() const
+ {
+ return this->limit_ - this->space() == 0;
+ }
+
private:
- qsizetype space()
+ qsizetype space() const
{
size_t totalSize = 0;
for (auto &chunk : *this->chunks_)
diff --git a/src/messages/Link.cpp b/src/messages/Link.cpp
index 7b5bcdb7d..e87c214a3 100644
--- a/src/messages/Link.cpp
+++ b/src/messages/Link.cpp
@@ -19,4 +19,9 @@ bool Link::isValid() const
return this->type != None;
}
+bool Link::isUrl() const
+{
+ return this->type == Url;
+}
+
} // namespace chatterino
diff --git a/src/messages/Link.hpp b/src/messages/Link.hpp
index 52a52444b..4545ec8cf 100644
--- a/src/messages/Link.hpp
+++ b/src/messages/Link.hpp
@@ -28,6 +28,7 @@ public:
QString value;
bool isValid() const;
+ bool isUrl() const;
};
} // namespace chatterino
diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp
index 218fa3a73..912eeefec 100644
--- a/src/messages/Message.cpp
+++ b/src/messages/Message.cpp
@@ -35,6 +35,12 @@ SBHighlight Message::getScrollBarHighlight() const
return SBHighlight(
ColorProvider::instance().color(ColorType::Subscription));
}
+ else if (this->flags.has(MessageFlag::RedeemedHighlight))
+ {
+ return SBHighlight(
+ ColorProvider::instance().color(ColorType::RedeemedHighlight),
+ SBHighlight::Default, true);
+ }
return SBHighlight();
}
diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp
index 3b5bb97a8..b5c97b553 100644
--- a/src/messages/Message.hpp
+++ b/src/messages/Message.hpp
@@ -34,6 +34,8 @@ enum class MessageFlag : uint32_t {
HighlightedWhisper = (1 << 17),
Debug = (1 << 18),
Similar = (1 << 19),
+ RedeemedHighlight = (1 << 20),
+ RedeemedChannelPointReward = (1 << 21),
};
using MessageFlags = FlagsEnum;
diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp
index 6e4b8115e..545d8735f 100644
--- a/src/messages/MessageBuilder.cpp
+++ b/src/messages/MessageBuilder.cpp
@@ -2,6 +2,7 @@
#include "Application.hpp"
#include "common/LinkParser.hpp"
+#include "controllers/accounts/AccountController.hpp"
#include "messages/Image.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
@@ -23,6 +24,11 @@ MessagePtr makeSystemMessage(const QString &text)
return MessageBuilder(systemMessage, text).release();
}
+MessagePtr makeSystemMessage(const QString &text, const QTime &time)
+{
+ return MessageBuilder(systemMessage, text, time).release();
+}
+
std::pair makeAutomodMessage(
const AutomodAction &action)
{
@@ -92,10 +98,11 @@ MessageBuilder::MessageBuilder()
{
}
-MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
+MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text,
+ const QTime &time)
: MessageBuilder()
{
- this->emplace();
+ this->emplace(time);
// check system message for links
// (e.g. needed for sub ticket message in sub only mode)
@@ -119,17 +126,40 @@ MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
this->message().searchText = text;
}
+MessageBuilder::MessageBuilder(TimeoutMessageTag,
+ const QString &systemMessageText, int times)
+ : MessageBuilder()
+{
+ QString username = systemMessageText.split(" ").at(0);
+ QString remainder = systemMessageText.mid(username.length() + 1);
+
+ QString text;
+
+ this->emplace();
+ this->emplaceSystemTextAndUpdate(username, text)
+ ->setLink({Link::UserInfo, username});
+ this->emplaceSystemTextAndUpdate(
+ QString("%1 (%2 times)").arg(remainder.trimmed()).arg(times), text);
+
+ this->message().messageText = text;
+ this->message().searchText = text;
+}
+
MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds,
const QString &reason, bool multipleTimes)
: MessageBuilder()
{
+ QString fullText;
QString text;
- text.append(username);
+ this->emplace();
+ this->emplaceSystemTextAndUpdate(username, fullText)
+ ->setLink({Link::UserInfo, username});
+
if (!durationInSeconds.isEmpty())
{
- text.append(" has been timed out");
+ text.append("has been timed out");
// TODO: Implement who timed the user out
@@ -143,7 +173,7 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
}
else
{
- text.append(" has been permanently banned");
+ text.append("has been permanently banned");
}
if (reason.length() > 0)
@@ -163,16 +193,18 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
this->message().flags.set(MessageFlag::Timeout);
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
this->message().timeoutUser = username;
- this->emplace();
- this->emplace(text, MessageElementFlag::Text,
- MessageColor::System);
- this->message().messageText = text;
- this->message().searchText = text;
+
+ this->emplaceSystemTextAndUpdate(text, fullText);
+ this->message().messageText = fullText;
+ this->message().searchText = fullText;
}
+// XXX: This does not belong in the MessageBuilder, this should be part of the TwitchMessageBuilder
MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
: MessageBuilder()
{
+ auto current = getApp()->accounts->twitch.getCurrent();
+
this->emplace();
this->message().flags.set(MessageFlag::System);
this->message().flags.set(MessageFlag::Timeout);
@@ -181,48 +213,84 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
QString text;
- if (action.isBan())
+ if (action.target.id == current->getUserId())
{
- if (action.reason.isEmpty())
+ this->emplaceSystemTextAndUpdate("You were", text);
+ if (action.isBan())
{
- text = QString("%1 banned %2.") //
- .arg(action.source.name)
- .arg(action.target.name);
+ this->emplaceSystemTextAndUpdate("banned", text);
}
else
{
- text = QString("%1 banned %2: \"%3\".") //
- .arg(action.source.name)
- .arg(action.target.name)
- .arg(action.reason);
+ this->emplaceSystemTextAndUpdate(
+ QString("timed out for %1").arg(formatTime(action.duration)),
+ text);
+ }
+
+ if (!action.source.name.isEmpty())
+ {
+ this->emplaceSystemTextAndUpdate("by", text);
+ this->emplaceSystemTextAndUpdate(
+ action.source.name + (action.reason.isEmpty() ? "." : ":"),
+ text)
+ ->setLink({Link::UserInfo, action.source.name});
+ }
+
+ if (!action.reason.isEmpty())
+ {
+ this->emplaceSystemTextAndUpdate(
+ QString("\"%1\".").arg(action.reason), text);
}
}
else
{
- if (action.reason.isEmpty())
+ if (action.isBan())
{
- text = QString("%1 timed out %2 for %3.") //
- .arg(action.source.name)
- .arg(action.target.name)
- .arg(formatTime(action.duration));
+ this->emplaceSystemTextAndUpdate(action.source.name, text)
+ ->setLink({Link::UserInfo, action.source.name});
+ this->emplaceSystemTextAndUpdate("banned", text);
+ if (action.reason.isEmpty())
+ {
+ this->emplaceSystemTextAndUpdate(action.target.name, text)
+ ->setLink({Link::UserInfo, action.target.name});
+ }
+ else
+ {
+ this->emplaceSystemTextAndUpdate(action.target.name + ":", text)
+ ->setLink({Link::UserInfo, action.target.name});
+ this->emplaceSystemTextAndUpdate(
+ QString("\"%1\".").arg(action.reason), text);
+ }
}
else
{
- text = QString("%1 timed out %2 for %3: \"%4\".") //
- .arg(action.source.name)
- .arg(action.target.name)
- .arg(formatTime(action.duration))
- .arg(action.reason);
- }
+ this->emplaceSystemTextAndUpdate(action.source.name, text)
+ ->setLink({Link::UserInfo, action.source.name});
+ this->emplaceSystemTextAndUpdate("timed out", text);
+ this->emplaceSystemTextAndUpdate(action.target.name, text)
+ ->setLink({Link::UserInfo, action.target.name});
+ if (action.reason.isEmpty())
+ {
+ this->emplaceSystemTextAndUpdate(
+ QString("for %1.").arg(formatTime(action.duration)), text);
+ }
+ else
+ {
+ this->emplaceSystemTextAndUpdate(
+ QString("for %1: \"%2\".")
+ .arg(formatTime(action.duration))
+ .arg(action.reason),
+ text);
+ }
- if (count > 1)
- {
- text.append(QString(" (%1 times)").arg(count));
+ if (count > 1)
+ {
+ this->emplaceSystemTextAndUpdate(
+ QString("(%1 times)").arg(count), text);
+ }
}
}
- this->emplace(text, MessageElementFlag::Text,
- MessageColor::System);
this->message().messageText = text;
this->message().searchText = text;
}
@@ -236,14 +304,15 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
this->message().timeoutUser = action.target.name;
- QString text =
- QString("%1 %2 %3.")
- .arg(action.source.name)
- .arg(QString(action.wasBan() ? "unbanned" : "untimedout"))
- .arg(action.target.name);
+ QString text;
+
+ this->emplaceSystemTextAndUpdate(action.source.name, text)
+ ->setLink({Link::UserInfo, action.source.name});
+ this->emplaceSystemTextAndUpdate(
+ action.wasBan() ? "unbanned" : "untimedout", text);
+ this->emplaceSystemTextAndUpdate(action.target.name, text)
+ ->setLink({Link::UserInfo, action.target.name});
- this->emplace(text, MessageElementFlag::Text,
- MessageColor::System);
this->message().messageText = text;
this->message().searchText = text;
}
@@ -384,7 +453,8 @@ void MessageBuilder::addLink(const QString &origLink,
LinkResolver::getLinkInfo(
matchedLink, nullptr,
[weakMessage = this->weakOf(), linkMELowercase, linkMEOriginal,
- matchedLink](QString tooltipText, Link originalLink) {
+ matchedLink](QString tooltipText, Link originalLink,
+ ImagePtr thumbnail) {
auto shared = weakMessage.lock();
if (!shared)
{
@@ -401,7 +471,21 @@ void MessageBuilder::addLink(const QString &origLink,
linkMELowercase->setLink(originalLink)->updateLink();
linkMEOriginal->setLink(originalLink)->updateLink();
}
+ linkMELowercase->setThumbnail(thumbnail);
+ linkMELowercase->setThumbnailType(
+ MessageElement::ThumbnailType::Link_Thumbnail);
+ linkMEOriginal->setThumbnail(thumbnail);
+ linkMEOriginal->setThumbnailType(
+ MessageElement::ThumbnailType::Link_Thumbnail);
});
}
+TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text,
+ QString &toUpdate)
+{
+ toUpdate.append(text + " ");
+ return this->emplace(text, MessageElementFlag::Text,
+ MessageColor::System);
+}
+
} // namespace chatterino
diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp
index 4793ce9d9..a8e04b628 100644
--- a/src/messages/MessageBuilder.hpp
+++ b/src/messages/MessageBuilder.hpp
@@ -22,6 +22,7 @@ const SystemMessageTag systemMessage{};
const TimeoutMessageTag timeoutMessage{};
MessagePtr makeSystemMessage(const QString &text);
+MessagePtr makeSystemMessage(const QString &text, const QTime &time);
std::pair makeAutomodMessage(
const AutomodAction &action);
@@ -31,14 +32,17 @@ struct MessageParseArgs {
bool isSentWhisper = false;
bool trimSubscriberUsername = false;
bool isStaffOrBroadcaster = false;
+ QString channelPointRewardId = "";
};
class MessageBuilder
-
{
public:
MessageBuilder();
- MessageBuilder(SystemMessageTag, const QString &text);
+ MessageBuilder(SystemMessageTag, const QString &text,
+ const QTime &time = QTime::currentTime());
+ MessageBuilder(TimeoutMessageTag, const QString &systemMessageText,
+ int times);
MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds, const QString &reason,
bool multipleTimes);
@@ -68,6 +72,13 @@ public:
}
private:
+ // Helper method that emplaces some text stylized as system text
+ // and then appends that text to the QString parameter "toUpdate".
+ // Returns the TextElement that was emplaced.
+ TextElement *emplaceSystemTextAndUpdate(const QString &text,
+ QString &toUpdate);
+
std::shared_ptr message_;
};
+
} // namespace chatterino
diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp
index 0defed597..e3318511a 100644
--- a/src/messages/MessageElement.cpp
+++ b/src/messages/MessageElement.cpp
@@ -1,7 +1,7 @@
#include "messages/MessageElement.hpp"
#include "Application.hpp"
-#include "controllers/moderationactions/ModerationActions.hpp"
+#include "common/IrcColors.hpp"
#include "debug/Benchmark.hpp"
#include "messages/Emote.hpp"
#include "messages/layouts/MessageLayoutContainer.hpp"
@@ -12,6 +12,14 @@
namespace chatterino {
+namespace {
+
+ QRegularExpression IRC_COLOR_PARSE_REGEX(
+ "(\u0003(\\d{1,2})?(,(\\d{1,2}))?|\u000f)",
+ QRegularExpression::UseUnicodePropertiesOption);
+
+} // namespace
+
MessageElement::MessageElement(MessageElementFlags flags)
: flags_(flags)
{
@@ -41,6 +49,18 @@ MessageElement *MessageElement::setTooltip(const QString &tooltip)
return this;
}
+MessageElement *MessageElement::setThumbnail(const ImagePtr &thumbnail)
+{
+ this->thumbnail_ = thumbnail;
+ return this;
+}
+
+MessageElement *MessageElement::setThumbnailType(const ThumbnailType type)
+{
+ this->thumbnailType_ = type;
+ return this;
+}
+
MessageElement *MessageElement::setTrailingSpace(bool value)
{
this->trailingSpace = value;
@@ -52,6 +72,16 @@ const QString &MessageElement::getTooltip() const
return this->tooltip_;
}
+const ImagePtr &MessageElement::getThumbnail() const
+{
+ return this->thumbnail_;
+}
+
+const MessageElement::ThumbnailType &MessageElement::getThumbnailType() const
+{
+ return this->thumbnailType_;
+}
+
const Link &MessageElement::getLink() const
{
return this->link_;
@@ -165,21 +195,6 @@ MessageLayoutElement *EmoteElement::makeImageLayoutElement(
return new ImageLayoutElement(*this, image, size);
}
-// MOD BADGE
-ModBadgeElement::ModBadgeElement(const EmotePtr &data,
- MessageElementFlags flags_)
- : EmoteElement(data, flags_)
-{
-}
-
-MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
- const ImagePtr &image, const QSize &size)
-{
- static const QColor modBadgeBackgroundColor("#34AE0A");
- return new ImageWithBackgroundLayoutElement(*this, image, size,
- modBadgeBackgroundColor);
-}
-
// BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags)
@@ -201,8 +216,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container,
auto size = QSize(int(container.getScale() * image->width()),
int(container.getScale() * image->height()));
- container.addElement((new ImageLayoutElement(*this, image, size))
- ->setLink(this->getLink()));
+ container.addElement(this->makeImageLayoutElement(image, size));
}
}
@@ -211,6 +225,34 @@ EmotePtr BadgeElement::getEmote() const
return this->emote_;
}
+MessageLayoutElement *BadgeElement::makeImageLayoutElement(
+ const ImagePtr &image, const QSize &size)
+{
+ auto element =
+ (new ImageLayoutElement(*this, image, size))->setLink(this->getLink());
+
+ return element;
+}
+
+// MOD BADGE
+ModBadgeElement::ModBadgeElement(const EmotePtr &data,
+ MessageElementFlags flags_)
+ : BadgeElement(data, flags_)
+{
+}
+
+MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
+ const ImagePtr &image, const QSize &size)
+{
+ static const QColor modBadgeBackgroundColor("#34AE0A");
+
+ auto element = (new ImageWithBackgroundLayoutElement(
+ *this, image, size, modBadgeBackgroundColor))
+ ->setLink(this->getLink());
+
+ return element;
+}
+
// TEXT
TextElement::TextElement(const QString &text, MessageElementFlags flags,
const MessageColor &color, FontStyle style)
@@ -319,10 +361,9 @@ void TextElement::addToContainer(MessageLayoutContainer &container,
if (isSurrogate)
i++;
}
-
- container.addElement(getTextLayoutElement(
+ //add the final piece of wrapped text
+ container.addElementNoLineBreak(getTextLayoutElement(
text.mid(wordStart), width, this->hasTrailingSpace()));
- container.breakLine();
}
}
}
@@ -374,8 +415,8 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
{
QSize size(int(container.getScale() * 16),
int(container.getScale() * 16));
-
- for (const auto &action : getApp()->moderationActions->items)
+ auto actions = getCSettings().moderationActions.readOnly();
+ for (const auto &action : *actions)
{
if (auto image = action.getImage())
{
@@ -395,4 +436,283 @@ void TwitchModerationElement::addToContainer(MessageLayoutContainer &container,
}
}
+// TEXT
+// IrcTextElement gets its color from the color code in the message, and can change from character to character.
+// This differs from the TextElement
+IrcTextElement::IrcTextElement(const QString &fullText,
+ MessageElementFlags flags, FontStyle style)
+ : MessageElement(flags)
+ , style_(style)
+{
+ assert(IRC_COLOR_PARSE_REGEX.isValid());
+
+ // Default pen colors. -1 = default theme colors
+ int fg = -1, bg = -1;
+
+ // Split up the message in words (space separated)
+ // Each word contains one or more colored segments.
+ // The color of that segment is "global", as in it can be decided by the word before it.
+ for (const auto &text : fullText.split(' '))
+ {
+ std::vector segments;
+
+ int pos = 0;
+ int lastPos = 0;
+
+ auto i = IRC_COLOR_PARSE_REGEX.globalMatch(text);
+
+ while (i.hasNext())
+ {
+ auto match = i.next();
+
+ if (lastPos != match.capturedStart() && match.capturedStart() != 0)
+ {
+ auto seg = Segment{};
+ seg.text = text.mid(lastPos, match.capturedStart() - lastPos);
+ seg.fg = fg;
+ seg.bg = bg;
+ segments.emplace_back(seg);
+ lastPos = match.capturedStart() + match.capturedLength();
+ }
+ if (!match.captured(1).isEmpty())
+ {
+ fg = -1;
+ bg = -1;
+ }
+
+ if (!match.captured(2).isEmpty())
+ {
+ fg = match.captured(2).toInt(nullptr);
+ }
+ else
+ {
+ fg = -1;
+ }
+ if (!match.captured(4).isEmpty())
+ {
+ bg = match.captured(4).toInt(nullptr);
+ }
+ else if (fg == -1)
+ {
+ bg = -1;
+ }
+
+ lastPos = match.capturedStart() + match.capturedLength();
+ }
+
+ auto seg = Segment{};
+ seg.text = text.mid(lastPos);
+ seg.fg = fg;
+ seg.bg = bg;
+ segments.emplace_back(seg);
+
+ QString n(text);
+
+ n.replace(IRC_COLOR_PARSE_REGEX, "");
+
+ Word w{
+ n,
+ -1,
+ segments,
+ };
+ this->words_.emplace_back(w);
+ }
+}
+
+void IrcTextElement::addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags)
+{
+ auto app = getApp();
+
+ MessageColor defaultColorType = MessageColor::Text;
+ auto defaultColor = defaultColorType.getColor(*app->themes);
+ if (flags.hasAny(this->getFlags()))
+ {
+ QFontMetrics metrics =
+ app->fonts->getFontMetrics(this->style_, container.getScale());
+
+ for (auto &word : this->words_)
+ {
+ auto getTextLayoutElement = [&](QString text,
+ std::vector segments,
+ int width, bool hasTrailingSpace) {
+ std::vector xd{};
+
+ for (const auto &segment : segments)
+ {
+ QColor color = defaultColor;
+ if (segment.fg >= 0 && segment.fg <= 98)
+ {
+ color = IRC_COLORS[segment.fg];
+ }
+ app->themes->normalizeColor(color);
+ xd.emplace_back(PajSegment{segment.text, color});
+ }
+
+ auto e = (new MultiColorTextLayoutElement(
+ *this, text, QSize(width, metrics.height()), xd,
+ this->style_, container.getScale()))
+ ->setLink(this->getLink());
+ e->setTrailingSpace(true);
+ e->setText(text);
+
+ // If URL link was changed,
+ // Should update it in MessageLayoutElement too!
+ if (this->getLink().type == Link::Url)
+ {
+ static_cast(e)->listenToLinkChanges();
+ }
+ return e;
+ };
+
+ // fourtf: add again
+ // if (word.width == -1) {
+ word.width = metrics.width(word.text);
+ // }
+
+ // see if the text fits in the current line
+ if (container.fitsInLine(word.width))
+ {
+ container.addElementNoLineBreak(
+ getTextLayoutElement(word.text, word.segments, word.width,
+ this->hasTrailingSpace()));
+ continue;
+ }
+
+ // see if the text fits in the next line
+ if (!container.atStartOfLine())
+ {
+ container.breakLine();
+
+ if (container.fitsInLine(word.width))
+ {
+ container.addElementNoLineBreak(getTextLayoutElement(
+ word.text, word.segments, word.width,
+ this->hasTrailingSpace()));
+ continue;
+ }
+ }
+
+ // we done goofed, we need to wrap the text
+ QString text = word.text;
+ std::vector segments = word.segments;
+ int textLength = text.length();
+ int wordStart = 0;
+ int width = 0;
+
+ // QChar::isHighSurrogate(text[0].unicode()) ? 2 : 1
+
+ // XXX(pajlada): NOT TESTED
+ for (int i = 0; i < textLength; i++) //
+ {
+ auto isSurrogate = text.size() > i + 1 &&
+ QChar::isHighSurrogate(text[i].unicode());
+
+ auto charWidth = isSurrogate ? metrics.width(text.mid(i, 2))
+ : metrics.width(text[i]);
+
+ if (!container.fitsInLine(width + charWidth))
+ {
+ std::vector pieceSegments;
+ int charactersLeft = i - wordStart;
+ assert(charactersLeft > 0);
+ for (auto segmentIt = segments.begin();
+ segmentIt != segments.end();)
+ {
+ assert(charactersLeft > 0);
+ auto &segment = *segmentIt;
+ if (charactersLeft >= segment.text.length())
+ {
+ // Entire segment fits in this piece
+ pieceSegments.push_back(segment);
+ charactersLeft -= segment.text.length();
+ segmentIt = segments.erase(segmentIt);
+
+ assert(charactersLeft >= 0);
+
+ if (charactersLeft == 0)
+ {
+ break;
+ }
+ }
+ else
+ {
+ // Only part of the segment fits in this piece
+ // We create a new segment with the characters that fit, and modify the segment we checked to only contain the characters we didn't consume
+ Segment segmentThatFitsInPiece{
+ segment.text.left(charactersLeft), segment.fg,
+ segment.bg};
+ pieceSegments.emplace_back(segmentThatFitsInPiece);
+ segment.text = segment.text.mid(charactersLeft);
+
+ break;
+ }
+ }
+
+ container.addElementNoLineBreak(
+ getTextLayoutElement(text.mid(wordStart, i - wordStart),
+ pieceSegments, width, false));
+ container.breakLine();
+
+ wordStart = i;
+ width = charWidth;
+
+ if (isSurrogate)
+ i++;
+ continue;
+ }
+
+ width += charWidth;
+
+ if (isSurrogate)
+ i++;
+ }
+
+ // Add last remaining text & segments
+ container.addElementNoLineBreak(
+ getTextLayoutElement(text.mid(wordStart), segments, width,
+ this->hasTrailingSpace()));
+ }
+ }
+}
+
+LinebreakElement::LinebreakElement(MessageElementFlags flags)
+ : MessageElement(flags)
+{
+}
+
+void LinebreakElement::addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags)
+{
+ if (flags.hasAny(this->getFlags()))
+ {
+ container.breakLine();
+ }
+}
+
+ScalingImageElement::ScalingImageElement(ImageSet images,
+ MessageElementFlags flags)
+ : MessageElement(flags)
+ , images_(images)
+{
+}
+
+void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags)
+{
+ if (flags.hasAny(this->getFlags()))
+ {
+ const auto &image =
+ this->images_.getImageOrLoaded(container.getScale());
+ if (image->isEmpty())
+ return;
+
+ auto size = QSize(image->width() * container.getScale(),
+ image->height() * container.getScale());
+
+ container.addElement((new ImageLayoutElement(*this, image, size))
+ ->setLink(this->getLink()));
+ }
+}
+
} // namespace chatterino
diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp
index 7d1a00c6e..8da2bdf9c 100644
--- a/src/messages/MessageElement.hpp
+++ b/src/messages/MessageElement.hpp
@@ -4,6 +4,7 @@
#include "messages/Link.hpp"
#include "messages/MessageColor.hpp"
#include "singletons/Fonts.hpp"
+#include "src/messages/ImageSet.hpp"
#include
#include
@@ -39,6 +40,10 @@ enum class MessageElementFlag {
BttvEmoteImage = (1 << 6),
BttvEmoteText = (1 << 7),
BttvEmote = BttvEmoteImage | BttvEmoteText,
+
+ ChannelPointReward = (1 << 8),
+ ChannelPointRewardImage = ChannelPointReward | TwitchEmoteImage,
+
FfzEmoteImage = (1 << 10),
FfzEmoteText = (1 << 11),
FfzEmote = FfzEmoteImage | FfzEmoteText,
@@ -122,14 +127,23 @@ public:
Update_Images = 4,
Update_All = Update_Text | Update_Emotes | Update_Images
};
+ enum ThumbnailType : char {
+ Link_Thumbnail = 1,
+ };
virtual ~MessageElement();
MessageElement *setLink(const Link &link);
MessageElement *setText(const QString &text);
MessageElement *setTooltip(const QString &tooltip);
+ MessageElement *setThumbnailType(const ThumbnailType type);
+ MessageElement *setThumbnail(const ImagePtr &thumbnail);
+
MessageElement *setTrailingSpace(bool value);
const QString &getTooltip() const;
+ const ImagePtr &getThumbnail() const;
+ const ThumbnailType &getThumbnailType() const;
+
const Link &getLink() const;
bool hasTrailingSpace() const;
MessageElementFlags getFlags() const;
@@ -148,6 +162,8 @@ private:
QString text_;
Link link_;
QString tooltip_;
+ ImagePtr thumbnail_;
+ ThumbnailType thumbnailType_;
MessageElementFlags flags_;
};
@@ -223,17 +239,6 @@ private:
EmotePtr emote_;
};
-// Behaves like an emote element, except it creates a different image layout element that draws the mod badge background
-class ModBadgeElement : public EmoteElement
-{
-public:
- ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
-
-protected:
- MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
- const QSize &size) override;
-};
-
class BadgeElement : public MessageElement
{
public:
@@ -244,10 +249,24 @@ public:
EmotePtr getEmote() const;
+protected:
+ virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
+ const QSize &size);
+
private:
EmotePtr emote_;
};
+class ModBadgeElement : public BadgeElement
+{
+public:
+ ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
+
+protected:
+ MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
+ const QSize &size) override;
+};
+
// contains a text, formated depending on the preferences
class TimestampElement : public MessageElement
{
@@ -277,4 +296,56 @@ public:
MessageElementFlags flags) override;
};
+// contains a full message string that's split into words on space and parses irc colors that are then put into segments
+// these segments are later passed to "MultiColorTextLayoutElement" elements to be rendered :)
+class IrcTextElement : public MessageElement
+{
+public:
+ IrcTextElement(const QString &text, MessageElementFlags flags,
+ FontStyle style = FontStyle::ChatMedium);
+ ~IrcTextElement() override = default;
+
+ void addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags) override;
+
+private:
+ FontStyle style_;
+
+ struct Segment {
+ QString text;
+ int fg = -1;
+ int bg = -1;
+ };
+
+ struct Word {
+ QString text;
+ int width = -1;
+ std::vector segments;
+ };
+
+ std::vector words_;
+};
+
+// Forces a linebreak
+class LinebreakElement : public MessageElement
+{
+public:
+ LinebreakElement(MessageElementFlags flags);
+
+ void addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags) override;
+};
+
+// Image element which will pick the quality of the image based on ui scale
+class ScalingImageElement : public MessageElement
+{
+public:
+ ScalingImageElement(ImageSet images, MessageElementFlags flags);
+
+ void addToContainer(MessageLayoutContainer &container,
+ MessageElementFlags flags) override;
+
+private:
+ ImageSet images_;
+};
} // namespace chatterino
diff --git a/src/messages/SharedMessageBuilder.cpp b/src/messages/SharedMessageBuilder.cpp
new file mode 100644
index 000000000..26a2d85a5
--- /dev/null
+++ b/src/messages/SharedMessageBuilder.cpp
@@ -0,0 +1,399 @@
+#include "messages/SharedMessageBuilder.hpp"
+
+#include "Application.hpp"
+#include "controllers/ignores/IgnorePhrase.hpp"
+#include "messages/Message.hpp"
+#include "messages/MessageElement.hpp"
+#include "providers/twitch/TwitchCommon.hpp"
+#include "singletons/Settings.hpp"
+#include "singletons/WindowManager.hpp"
+
+namespace chatterino {
+
+namespace {
+
+ QUrl getFallbackHighlightSound()
+ {
+ QString path = getSettings()->pathHighlightSound;
+ bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
+
+ // Use fallback sound when checkbox is not checked
+ // or custom file doesn't exist
+ if (getSettings()->customHighlightSound && fileExists)
+ {
+ return QUrl::fromLocalFile(path);
+ }
+ else
+ {
+ return QUrl("qrc:/sounds/ping2.wav");
+ }
+ }
+
+} // namespace
+
+SharedMessageBuilder::SharedMessageBuilder(
+ Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
+ const MessageParseArgs &_args)
+ : channel(_channel)
+ , ircMessage(_ircMessage)
+ , args(_args)
+ , tags(this->ircMessage->tags())
+ , originalMessage_(_ircMessage->content())
+ , action_(_ircMessage->isAction())
+{
+}
+
+SharedMessageBuilder::SharedMessageBuilder(
+ Channel *_channel, const Communi::IrcMessage *_ircMessage,
+ const MessageParseArgs &_args, QString content, bool isAction)
+ : channel(_channel)
+ , ircMessage(_ircMessage)
+ , args(_args)
+ , tags(this->ircMessage->tags())
+ , originalMessage_(content)
+ , action_(isAction)
+{
+}
+
+namespace {
+
+ QColor getRandomColor(const QString &v)
+ {
+ int colorSeed = 0;
+ for (const auto &c : v)
+ {
+ colorSeed += c.digitValue();
+ }
+ const auto colorIndex = colorSeed % TWITCH_USERNAME_COLORS.size();
+ return TWITCH_USERNAME_COLORS[colorIndex];
+ }
+
+} // namespace
+
+void SharedMessageBuilder::parse()
+{
+ this->parseUsernameColor();
+
+ this->parseUsername();
+
+ this->message().flags.set(MessageFlag::Collapsed);
+}
+
+bool SharedMessageBuilder::isIgnored() const
+{
+ // TODO(pajlada): Do we need to check if the phrase is valid first?
+ auto phrases = getCSettings().ignoredMessages.readOnly();
+ for (const auto &phrase : *phrases)
+ {
+ if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
+ {
+ qDebug() << "Blocking message because it contains ignored phrase"
+ << phrase.getPattern();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SharedMessageBuilder::parseUsernameColor()
+{
+ if (getSettings()->colorizeNicknames)
+ {
+ this->usernameColor_ = getRandomColor(this->ircMessage->nick());
+ }
+}
+
+void SharedMessageBuilder::parseUsername()
+{
+ // username
+ this->userName = this->ircMessage->nick();
+
+ this->message().loginName = this->userName;
+}
+
+void SharedMessageBuilder::parseHighlights()
+{
+ auto app = getApp();
+
+ if (this->message().flags.has(MessageFlag::Subscription) &&
+ getSettings()->enableSubHighlight)
+ {
+ if (getSettings()->enableSubHighlightTaskbar)
+ {
+ this->highlightAlert_ = true;
+ }
+
+ if (getSettings()->enableSubHighlightSound)
+ {
+ this->highlightSound_ = true;
+
+ // Use custom sound if set, otherwise use fallback
+ if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
+ {
+ this->highlightSoundUrl_ =
+ QUrl(getSettings()->subHighlightSoundUrl.getValue());
+ }
+ else
+ {
+ this->highlightSoundUrl_ = getFallbackHighlightSound();
+ }
+ }
+
+ this->message().flags.set(MessageFlag::Highlighted);
+ this->message().highlightColor =
+ ColorProvider::instance().color(ColorType::Subscription);
+
+ // This message was a subscription.
+ // Don't check for any other highlight phrases.
+ return;
+ }
+
+ // XXX: Non-common term in SharedMessageBuilder
+ auto currentUser = app->accounts->twitch.getCurrent();
+
+ QString currentUsername = currentUser->getUserName();
+
+ if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
+ {
+ // Do nothing. We ignore highlights from this user.
+ return;
+ }
+
+ // Highlight because it's a whisper
+ if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
+ {
+ if (getSettings()->enableWhisperHighlightTaskbar)
+ {
+ this->highlightAlert_ = true;
+ }
+
+ if (getSettings()->enableWhisperHighlightSound)
+ {
+ this->highlightSound_ = true;
+
+ // Use custom sound if set, otherwise use fallback
+ if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
+ {
+ this->highlightSoundUrl_ =
+ QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
+ }
+ else
+ {
+ this->highlightSoundUrl_ = getFallbackHighlightSound();
+ }
+ }
+
+ this->message().highlightColor =
+ ColorProvider::instance().color(ColorType::Whisper);
+
+ /*
+ * Do _NOT_ return yet, we might want to apply phrase/user name
+ * highlights (which override whisper color/sound).
+ */
+ }
+
+ // Highlight because of sender
+ auto userHighlights = getCSettings().highlightedUsers.readOnly();
+ for (const HighlightPhrase &userHighlight : *userHighlights)
+ {
+ if (!userHighlight.isMatch(this->ircMessage->nick()))
+ {
+ continue;
+ }
+ qDebug() << "Highlight because user" << this->ircMessage->nick()
+ << "sent a message";
+
+ this->message().flags.set(MessageFlag::Highlighted);
+ this->message().highlightColor = userHighlight.getColor();
+
+ if (userHighlight.hasAlert())
+ {
+ this->highlightAlert_ = true;
+ }
+
+ if (userHighlight.hasSound())
+ {
+ this->highlightSound_ = true;
+ // Use custom sound if set, otherwise use the fallback sound
+ if (userHighlight.hasCustomSound())
+ {
+ this->highlightSoundUrl_ = userHighlight.getSoundUrl();
+ }
+ else
+ {
+ this->highlightSoundUrl_ = getFallbackHighlightSound();
+ }
+ }
+
+ if (this->highlightAlert_ && this->highlightSound_)
+ {
+ /*
+ * User name highlights "beat" highlight phrases: If a message has
+ * all attributes (color, taskbar flashing, sound) set, highlight
+ * phrases will not be checked.
+ */
+ return;
+ }
+ }
+
+ if (this->ircMessage->nick() == currentUsername)
+ {
+ // Do nothing. Highlights cannot be triggered by yourself
+ return;
+ }
+
+ // TODO: This vector should only be rebuilt upon highlights being changed
+ // fourtf: should be implemented in the HighlightsController
+ std::vector activeHighlights =
+ getSettings()->highlightedMessages.cloneVector();
+
+ if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
+ {
+ HighlightPhrase selfHighlight(
+ currentUsername, getSettings()->enableSelfHighlightTaskbar,
+ getSettings()->enableSelfHighlightSound, false, false,
+ getSettings()->selfHighlightSoundUrl.getValue(),
+ ColorProvider::instance().color(ColorType::SelfHighlight));
+ activeHighlights.emplace_back(std::move(selfHighlight));
+ }
+
+ // Highlight because of message
+ for (const HighlightPhrase &highlight : activeHighlights)
+ {
+ if (!highlight.isMatch(this->originalMessage_))
+ {
+ continue;
+ }
+
+ this->message().flags.set(MessageFlag::Highlighted);
+ this->message().highlightColor = highlight.getColor();
+
+ if (highlight.hasAlert())
+ {
+ this->highlightAlert_ = true;
+ }
+
+ // Only set highlightSound_ if it hasn't been set by username
+ // highlights already.
+ if (highlight.hasSound() && !this->highlightSound_)
+ {
+ this->highlightSound_ = true;
+
+ // Use custom sound if set, otherwise use fallback sound
+ if (highlight.hasCustomSound())
+ {
+ this->highlightSoundUrl_ = highlight.getSoundUrl();
+ }
+ else
+ {
+ this->highlightSoundUrl_ = getFallbackHighlightSound();
+ }
+ }
+
+ if (this->highlightAlert_ && this->highlightSound_)
+ {
+ /*
+ * Break once no further attributes (taskbar, sound) can be
+ * applied.
+ */
+ break;
+ }
+ }
+}
+
+void SharedMessageBuilder::addTextOrEmoji(EmotePtr emote)
+{
+ this->emplace(emote, MessageElementFlag::EmojiAll);
+}
+
+void SharedMessageBuilder::addTextOrEmoji(const QString &string_)
+{
+ auto string = QString(string_);
+
+ // Actually just text
+ auto linkString = this->matchLink(string);
+ auto link = Link();
+ auto textColor = this->action_ ? MessageColor(this->usernameColor_)
+ : MessageColor(MessageColor::Text);
+
+ if (linkString.isEmpty())
+ {
+ if (string.startsWith('@'))
+ {
+ this->emplace(string, MessageElementFlag::BoldUsername,
+ textColor, FontStyle::ChatMediumBold);
+ this->emplace(
+ string, MessageElementFlag::NonBoldUsername, textColor);
+ }
+ else
+ {
+ this->emplace(string, MessageElementFlag::Text,
+ textColor);
+ }
+ }
+ else
+ {
+ this->addLink(string, linkString);
+ }
+}
+
+void SharedMessageBuilder::appendChannelName()
+{
+ QString channelName("#" + this->channel->getName());
+ Link link(Link::Url, this->channel->getName() + "\n" + this->message().id);
+
+ this->emplace(channelName, MessageElementFlag::ChannelName,
+ MessageColor::System) //
+ ->setLink(link);
+}
+
+inline QMediaPlayer *getPlayer()
+{
+ if (isGuiThread())
+ {
+ static auto player = new QMediaPlayer;
+ return player;
+ }
+ else
+ {
+ return nullptr;
+ }
+}
+
+void SharedMessageBuilder::triggerHighlights()
+{
+ static QUrl currentPlayerUrl;
+
+ if (getCSettings().isMutedChannel(this->channel->getName()))
+ {
+ // Do nothing. Pings are muted in this channel.
+ return;
+ }
+
+ bool hasFocus = (QApplication::focusWidget() != nullptr);
+ bool resolveFocus = !hasFocus || getSettings()->highlightAlwaysPlaySound;
+
+ if (this->highlightSound_ && resolveFocus)
+ {
+ if (auto player = getPlayer())
+ {
+ // update the media player url if necessary
+ if (currentPlayerUrl != this->highlightSoundUrl_)
+ {
+ player->setMedia(this->highlightSoundUrl_);
+
+ currentPlayerUrl = this->highlightSoundUrl_;
+ }
+
+ player->play();
+ }
+ }
+
+ if (this->highlightAlert_)
+ {
+ getApp()->windows->sendAlert();
+ }
+}
+
+} // namespace chatterino
diff --git a/src/messages/SharedMessageBuilder.hpp b/src/messages/SharedMessageBuilder.hpp
new file mode 100644
index 000000000..355c2908d
--- /dev/null
+++ b/src/messages/SharedMessageBuilder.hpp
@@ -0,0 +1,69 @@
+#include "messages/MessageBuilder.hpp"
+
+#include "common/Aliases.hpp"
+#include "common/Outcome.hpp"
+
+#include
+#include
+
+namespace chatterino {
+
+class SharedMessageBuilder : public MessageBuilder
+{
+public:
+ SharedMessageBuilder() = delete;
+
+ explicit SharedMessageBuilder(Channel *_channel,
+ const Communi::IrcPrivateMessage *_ircMessage,
+ const MessageParseArgs &_args);
+
+ explicit SharedMessageBuilder(Channel *_channel,
+ const Communi::IrcMessage *_ircMessage,
+ const MessageParseArgs &_args,
+ QString content, bool isAction);
+
+ QString userName;
+
+ [[nodiscard]] virtual bool isIgnored() const;
+
+ // triggerHighlights triggers any alerts or sounds parsed by parseHighlights
+ virtual void triggerHighlights();
+ virtual MessagePtr build() = 0;
+
+protected:
+ virtual void parse();
+
+ virtual void parseUsernameColor();
+
+ virtual void parseUsername();
+
+ virtual Outcome tryAppendEmote(const EmoteName &name)
+ {
+ return Failure;
+ }
+
+ // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function
+ virtual void parseHighlights();
+
+ virtual void addTextOrEmoji(EmotePtr emote);
+ virtual void addTextOrEmoji(const QString &value);
+
+ void appendChannelName();
+
+ Channel *channel;
+ const Communi::IrcMessage *ircMessage;
+ MessageParseArgs args;
+ const QVariantMap tags;
+ QString originalMessage_;
+
+ const bool action_{};
+
+ QColor usernameColor_;
+
+ bool highlightAlert_ = false;
+ bool highlightSound_ = false;
+
+ QUrl highlightSoundUrl_;
+};
+
+} // namespace chatterino
diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp
index c048bd626..0e7547ffa 100644
--- a/src/messages/layouts/MessageLayout.cpp
+++ b/src/messages/layouts/MessageLayout.cpp
@@ -169,7 +169,7 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
// Painting
void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage,
- bool isWindowFocused)
+ bool isWindowFocused, bool isMentions)
{
auto app = getApp();
QPixmap *pixmap = this->buffer_.get();
@@ -220,6 +220,14 @@ void MessageLayout::paint(QPainter &painter, int width, int y, int messageIndex,
app->themes->messages.disabled);
}
+ if (!isMentions &&
+ this->message_->flags.has(MessageFlag::RedeemedChannelPointReward))
+ {
+ painter.fillRect(
+ 0, y, this->scale_ * 4, pixmap->height(),
+ *ColorProvider::instance().color(ColorType::Subscription));
+ }
+
// draw selection
if (!selection.isEmpty())
{
@@ -263,6 +271,7 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
Selection & /*selection*/)
{
auto app = getApp();
+ auto settings = getSettings();
QPainter painter(buffer);
painter.setRenderHint(QPainter::SmoothPixmapTransform);
@@ -296,6 +305,14 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
backgroundColor,
*ColorProvider::instance().color(ColorType::Subscription));
}
+ else if (this->message_->flags.has(MessageFlag::RedeemedHighlight) &&
+ settings->enableRedeemedHighlight.getValue())
+ {
+ // Blend highlight color with usual background color
+ backgroundColor = blendColors(
+ backgroundColor,
+ *ColorProvider::instance().color(ColorType::RedeemedHighlight));
+ }
else if (this->message_->flags.has(MessageFlag::AutoMod))
{
backgroundColor = QColor("#404040");
diff --git a/src/messages/layouts/MessageLayout.hpp b/src/messages/layouts/MessageLayout.hpp
index cd9bdeb5d..6048aca6b 100644
--- a/src/messages/layouts/MessageLayout.hpp
+++ b/src/messages/layouts/MessageLayout.hpp
@@ -47,7 +47,7 @@ public:
// Painting
void paint(QPainter &painter, int width, int y, int messageIndex,
Selection &selection, bool isLastReadMessage,
- bool isWindowFocused);
+ bool isWindowFocused, bool isMentions);
void invalidateBuffer();
void deleteBuffer();
void deleteCache();
diff --git a/src/messages/layouts/MessageLayoutContainer.cpp b/src/messages/layouts/MessageLayoutContainer.cpp
index 9c46e0c27..543b3f4e8 100644
--- a/src/messages/layouts/MessageLayoutContainer.cpp
+++ b/src/messages/layouts/MessageLayoutContainer.cpp
@@ -123,9 +123,20 @@ void MessageLayoutContainer::_addElement(MessageLayoutElement *element,
xOffset -= element->getRect().width() + this->spaceWidth_;
}
+ auto yOffset = 0;
+
+ if (element->getCreator().getFlags().has(
+ MessageElementFlag::ChannelPointReward) &&
+ element->getCreator().getFlags().hasNone(
+ {MessageElementFlag::TwitchEmoteImage}))
+ {
+ yOffset -= (this->margin.top * this->scale_);
+ }
+
// set move element
- element->setPosition(QPoint(this->currentX_ + xOffset,
- this->currentY_ - element->getRect().height()));
+ element->setPosition(
+ QPoint(this->currentX_ + xOffset,
+ this->currentY_ - element->getRect().height() + yOffset));
// add element
this->elements_.push_back(std::unique_ptr(element));
diff --git a/src/messages/layouts/MessageLayoutElement.cpp b/src/messages/layouts/MessageLayoutElement.cpp
index 7990d6a6c..49edde0ef 100644
--- a/src/messages/layouts/MessageLayoutElement.cpp
+++ b/src/messages/layouts/MessageLayoutElement.cpp
@@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index)
}
}
+//
+// TEXT
+//
+
+MultiColorTextLayoutElement::MultiColorTextLayoutElement(
+ MessageElement &_creator, QString &_text, const QSize &_size,
+ std::vector segments, FontStyle _style, float _scale)
+ : TextLayoutElement(_creator, _text, _size, QColor{}, _style, _scale)
+ , segments_(segments)
+{
+ this->setText(_text);
+}
+
+void MultiColorTextLayoutElement::paint(QPainter &painter)
+{
+ auto app = getApp();
+
+ painter.setPen(this->color_);
+
+ painter.setFont(app->fonts->getFont(this->style_, this->scale_));
+
+ int xOffset = 0;
+
+ auto metrics = app->fonts->getFontMetrics(this->style_, this->scale_);
+
+ for (const auto &segment : this->segments_)
+ {
+ // qDebug() << "Draw segment:" << segment.text;
+ painter.setPen(segment.color);
+ painter.drawText(QRectF(this->getRect().x() + xOffset,
+ this->getRect().y(), 10000, 10000),
+ segment.text,
+ QTextOption(Qt::AlignLeft | Qt::AlignTop));
+ xOffset += metrics.width(segment.text);
+ }
+}
+
} // namespace chatterino
diff --git a/src/messages/layouts/MessageLayoutElement.hpp b/src/messages/layouts/MessageLayoutElement.hpp
index 60d166a4c..fd711af88 100644
--- a/src/messages/layouts/MessageLayoutElement.hpp
+++ b/src/messages/layouts/MessageLayoutElement.hpp
@@ -107,7 +107,6 @@ protected:
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
-private:
QColor color_;
FontStyle style_;
float scale_;
@@ -138,4 +137,25 @@ private:
QString line2;
};
+struct PajSegment {
+ QString text;
+ QColor color;
+};
+
+// TEXT
+class MultiColorTextLayoutElement : public TextLayoutElement
+{
+public:
+ MultiColorTextLayoutElement(MessageElement &creator_, QString &text,
+ const QSize &size,
+ std::vector segments,
+ FontStyle style_, float scale_);
+
+protected:
+ void paint(QPainter &painter) override;
+
+private:
+ std::vector segments_;
+};
+
} // namespace chatterino
diff --git a/src/providers/LinkResolver.cpp b/src/providers/LinkResolver.cpp
index 50f6c9716..41f511eb5 100644
--- a/src/providers/LinkResolver.cpp
+++ b/src/providers/LinkResolver.cpp
@@ -3,6 +3,7 @@
#include "common/Common.hpp"
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp"
+#include "messages/Image.hpp"
#include "messages/Link.hpp"
#include "singletons/Settings.hpp"
@@ -12,11 +13,11 @@ namespace chatterino {
void LinkResolver::getLinkInfo(
const QString url, QObject *caller,
- std::function successCallback)
+ std::function successCallback)
{
if (!getSettings()->linkInfoTooltip)
{
- successCallback("No link info loaded", Link(Link::Url, url));
+ successCallback("No link info loaded", Link(Link::Url, url), nullptr);
return;
}
// Uncomment to test crashes
@@ -25,30 +26,35 @@ void LinkResolver::getLinkInfo(
QUrl::toPercentEncoding(url, "", "/:"))))
.caller(caller)
.timeout(30000)
- .onSuccess([successCallback, url](auto result) mutable -> Outcome {
- auto root = result.parseJson();
- auto statusCode = root.value("status").toInt();
- QString response = QString();
- QString linkString = url;
- if (statusCode == 200)
- {
- response = root.value("tooltip").toString();
- if (getSettings()->unshortLinks)
+ .onSuccess(
+ [successCallback, url](NetworkResult result) mutable -> Outcome {
+ auto root = result.parseJson();
+ auto statusCode = root.value("status").toInt();
+ QString response = QString();
+ QString linkString = url;
+ ImagePtr thumbnail = nullptr;
+ if (statusCode == 200)
{
- linkString = root.value("link").toString();
+ response = root.value("tooltip").toString();
+ thumbnail =
+ Image::fromUrl({root.value("thumbnail").toString()});
+ if (getSettings()->unshortLinks)
+ {
+ linkString = root.value("link").toString();
+ }
}
- }
- else
- {
- response = root.value("message").toString();
- }
- successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
- Link(Link::Url, linkString));
+ else
+ {
+ response = root.value("message").toString();
+ }
+ successCallback(QUrl::fromPercentEncoding(response.toUtf8()),
+ Link(Link::Url, linkString), thumbnail);
- return Success;
- })
+ return Success;
+ })
.onError([successCallback, url](auto /*result*/) {
- successCallback("No link info found", Link(Link::Url, url));
+ successCallback("No link info found", Link(Link::Url, url),
+ nullptr);
})
.execute();
// });
diff --git a/src/providers/LinkResolver.hpp b/src/providers/LinkResolver.hpp
index d4e9804f9..d1b24d690 100644
--- a/src/providers/LinkResolver.hpp
+++ b/src/providers/LinkResolver.hpp
@@ -3,6 +3,7 @@
#include
#include
+#include "messages/Image.hpp"
#include "messages/Link.hpp"
namespace chatterino {
@@ -10,8 +11,9 @@ namespace chatterino {
class LinkResolver
{
public:
- static void getLinkInfo(const QString url, QObject *caller,
- std::function callback);
+ static void getLinkInfo(
+ const QString url, QObject *caller,
+ std::function callback);
};
} // namespace chatterino
diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp
index d0c4c2a19..815e6afa4 100644
--- a/src/providers/bttv/BttvEmotes.cpp
+++ b/src/providers/bttv/BttvEmotes.cpp
@@ -8,6 +8,7 @@
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/ImageSet.hpp"
+#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino {
@@ -64,11 +65,12 @@ namespace {
return {Success, std::move(emotes)};
}
- std::pair parseChannelEmotes(const QJsonObject &jsonRoot)
+ std::pair parseChannelEmotes(const QJsonObject &jsonRoot,
+ const QString &userName)
{
auto emotes = EmoteMap();
- auto innerParse = [&jsonRoot, &emotes](const char *key) {
+ auto innerParse = [&jsonRoot, &emotes, &userName](const char *key) {
auto jsonEmotes = jsonRoot.value(key).toArray();
for (auto jsonEmote_ : jsonEmotes)
{
@@ -76,6 +78,10 @@ namespace {
auto id = EmoteId{jsonEmote.value("id").toString()};
auto name = EmoteName{jsonEmote.value("code").toString()};
+ auto author = EmoteAuthor{jsonEmote.value("user")
+ .toObject()
+ .value("name")
+ .toString()};
// emoteObject.value("imageType").toString();
auto emote = Emote({
@@ -85,7 +91,10 @@ namespace {
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
},
- Tooltip{name.string + "
Channel BetterTTV Emote"},
+ Tooltip{name.string + "
Channel BetterTTV Emote" +
+ ((author.string.isEmpty())
+ ? "
By: " + userName.toUtf8()
+ : "
By: " + author.string)},
Url{emoteLinkFormat.arg(id.string)},
});
@@ -138,17 +147,51 @@ void BttvEmotes::loadEmotes()
.execute();
}
-void BttvEmotes::loadChannel(const QString &channelId,
- std::function callback)
+void BttvEmotes::loadChannel(std::weak_ptr channel,
+ const QString &channelId, const QString &userName,
+ std::function callback,
+ bool manualRefresh)
{
NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelId)
.timeout(3000)
- .onSuccess([callback = std::move(callback)](auto result) -> Outcome {
- auto pair = parseChannelEmotes(result.parseJson());
+ .onSuccess([callback = std::move(callback), channel, &userName,
+ manualRefresh](auto result) -> Outcome {
+ auto pair = parseChannelEmotes(result.parseJson(), userName);
if (pair.first)
callback(std::move(pair.second));
+ if (auto shared = channel.lock(); manualRefresh)
+ shared->addMessage(
+ makeSystemMessage("BetterTTV channel emotes reloaded."));
return pair.first;
})
+ .onError([channelId, channel, manualRefresh](auto result) {
+ auto shared = channel.lock();
+ if (!shared)
+ return;
+ if (result.status() == 203)
+ {
+ // User does not have any BTTV emotes
+ if (manualRefresh)
+ shared->addMessage(makeSystemMessage(
+ "This channel has no BetterTTV channel emotes."));
+ }
+ else if (result.status() == NetworkResult::timedoutStatus)
+ {
+ // TODO: Auto retry in case of a timeout, with a delay
+ qDebug() << "Fetching BTTV emotes for channel" << channelId
+ << "failed due to timeout";
+ shared->addMessage(makeSystemMessage(
+ "Failed to fetch BetterTTV channel emotes. (timed out)"));
+ }
+ else
+ {
+ qDebug() << "Error fetching BTTV emotes for channel"
+ << channelId << ", error" << result.status();
+ shared->addMessage(
+ makeSystemMessage("Failed to fetch BetterTTV channel "
+ "emotes. (unknown error)"));
+ }
+ })
.execute();
}
diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp
index 9c16edeff..72b21aef4 100644
--- a/src/providers/bttv/BttvEmotes.hpp
+++ b/src/providers/bttv/BttvEmotes.hpp
@@ -4,6 +4,7 @@
#include "boost/optional.hpp"
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
+#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino {
@@ -24,8 +25,10 @@ public:
std::shared_ptr emotes() const;
boost::optional emote(const EmoteName &name) const;
void loadEmotes();
- static void loadChannel(const QString &channelId,
- std::function callback);
+ static void loadChannel(std::weak_ptr channel,
+ const QString &channelId, const QString &userName,
+ std::function callback,
+ bool manualRefresh);
private:
Atomic> global_;
diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp
index 492689aa0..ad35db056 100644
--- a/src/providers/colors/ColorProvider.cpp
+++ b/src/providers/colors/ColorProvider.cpp
@@ -1,6 +1,5 @@
#include "providers/colors/ColorProvider.hpp"
-#include "controllers/highlights/HighlightController.hpp"
#include "singletons/Theme.hpp"
namespace chatterino {
@@ -38,12 +37,12 @@ QSet ColorProvider::recentColors() const
* Currently, only colors used in highlight phrases are considered. This
* may change at any point in the future.
*/
- for (auto phrase : getApp()->highlights->phrases)
+ for (auto phrase : getSettings()->highlightedMessages)
{
retVal.insert(*phrase.getColor());
}
- for (auto userHl : getApp()->highlights->highlightedUsers)
+ for (auto userHl : getSettings()->highlightedUsers)
{
retVal.insert(*userHl.getColor());
}
@@ -65,7 +64,6 @@ void ColorProvider::initTypeColorMap()
{
// Read settings for custom highlight colors and save them in map.
// If no custom values can be found, set up default values instead.
- auto backgrounds = getApp()->themes->messages.backgrounds;
QString customColor = getSettings()->selfHighlightColor;
if (QColor(customColor).isValid())
@@ -77,7 +75,8 @@ void ColorProvider::initTypeColorMap()
{
this->typeColorMap_.insert(
{ColorType::SelfHighlight,
- std::make_shared(backgrounds.highlighted)});
+ std::make_shared(
+ HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
}
customColor = getSettings()->subHighlightColor;
@@ -90,7 +89,7 @@ void ColorProvider::initTypeColorMap()
{
this->typeColorMap_.insert(
{ColorType::Subscription,
- std::make_shared(backgrounds.subscription)});
+ std::make_shared(HighlightPhrase::FALLBACK_SUB_COLOR)});
}
customColor = getSettings()->whisperHighlightColor;
@@ -103,22 +102,41 @@ void ColorProvider::initTypeColorMap()
{
this->typeColorMap_.insert(
{ColorType::Whisper,
- std::make_shared(backgrounds.highlighted)});
+ std::make_shared(
+ HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
+ }
+
+ customColor = getSettings()->redeemedHighlightColor;
+ if (QColor(customColor).isValid())
+ {
+ this->typeColorMap_.insert({ColorType::RedeemedHighlight,
+ std::make_shared(customColor)});
+ }
+ else
+ {
+ this->typeColorMap_.insert(
+ {ColorType::RedeemedHighlight,
+ std::make_shared(
+ HighlightPhrase::FALLBACK_REDEEMED_HIGHLIGHT_COLOR)});
}
}
void ColorProvider::initDefaultColors()
{
// Init default colors
- this->defaultColors_.emplace_back(31, 141, 43, 127); // Green-ish
- this->defaultColors_.emplace_back(28, 126, 141, 127); // Blue-ish
- this->defaultColors_.emplace_back(136, 141, 49, 127); // Golden-ish
- this->defaultColors_.emplace_back(143, 48, 24, 127); // Red-ish
- this->defaultColors_.emplace_back(28, 141, 117, 127); // Cyan-ish
+ this->defaultColors_.emplace_back(75, 127, 107, 100); // Teal
+ this->defaultColors_.emplace_back(105, 127, 63, 100); // Olive
+ this->defaultColors_.emplace_back(63, 83, 127, 100); // Blue
+ this->defaultColors_.emplace_back(72, 127, 63, 100); // Green
- auto backgrounds = getApp()->themes->messages.backgrounds;
- this->defaultColors_.push_back(backgrounds.highlighted);
- this->defaultColors_.push_back(backgrounds.subscription);
+ this->defaultColors_.emplace_back(31, 141, 43, 115); // Green
+ this->defaultColors_.emplace_back(28, 126, 141, 90); // Blue
+ this->defaultColors_.emplace_back(136, 141, 49, 90); // Golden
+ this->defaultColors_.emplace_back(143, 48, 24, 127); // Red
+ this->defaultColors_.emplace_back(28, 141, 117, 90); // Cyan
+
+ this->defaultColors_.push_back(HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR);
+ this->defaultColors_.push_back(HighlightPhrase::FALLBACK_SUB_COLOR);
}
} // namespace chatterino
diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp
index e038b17f2..1595c53d6 100644
--- a/src/providers/colors/ColorProvider.hpp
+++ b/src/providers/colors/ColorProvider.hpp
@@ -2,7 +2,12 @@
namespace chatterino {
-enum class ColorType { SelfHighlight, Subscription, Whisper };
+enum class ColorType {
+ SelfHighlight,
+ Subscription,
+ Whisper,
+ RedeemedHighlight
+};
class ColorProvider
{
diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp
index 29dbac52d..dc1b1ab3c 100644
--- a/src/providers/ffz/FfzEmotes.cpp
+++ b/src/providers/ffz/FfzEmotes.cpp
@@ -6,6 +6,8 @@
#include "common/Outcome.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
+#include "messages/MessageBuilder.hpp"
+#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino {
namespace {
@@ -102,7 +104,7 @@ namespace {
modBadge = std::make_shared(Emote{
{""},
modBadgeImageSet,
- Tooltip{"Twitch Channel Moderator"},
+ Tooltip{"Moderator"},
modBadge1x,
});
}
@@ -126,11 +128,17 @@ namespace {
auto id =
EmoteId{QString::number(jsonEmote.value("id").toInt())};
auto name = EmoteName{jsonEmote.value("name").toString()};
+ auto author = EmoteAuthor{jsonEmote.value("owner")
+ .toObject()
+ .value("display_name")
+ .toString()};
auto urls = jsonEmote.value("urls").toObject();
Emote emote;
fillInEmoteData(urls, name,
- name.string + "
Channel FFZ Emote", emote);
+ name.string + "
Channel FFZ Emote" +
+ "
By: " + author.string,
+ emote);
emote.homePage =
Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
.arg(id.string)
@@ -182,8 +190,10 @@ void FfzEmotes::loadEmotes()
}
void FfzEmotes::loadChannel(
- const QString &channelId, std::function emoteCallback,
- std::function)> modBadgeCallback)
+ std::weak_ptr channel, const QString &channelId,
+ std::function emoteCallback,
+ std::function)> modBadgeCallback,
+ bool manualRefresh)
{
qDebug() << "[FFZEmotes] Reload FFZ Channel Emotes for channel"
<< channelId;
@@ -192,32 +202,47 @@ void FfzEmotes::loadChannel(
.timeout(20000)
.onSuccess([emoteCallback = std::move(emoteCallback),
- modBadgeCallback =
- std::move(modBadgeCallback)](auto result) -> Outcome {
+ modBadgeCallback = std::move(modBadgeCallback), channel,
+ manualRefresh](auto result) -> Outcome {
auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json);
auto modBadge = parseModBadge(json);
emoteCallback(std::move(emoteMap));
modBadgeCallback(std::move(modBadge));
+ if (auto shared = channel.lock(); manualRefresh)
+ shared->addMessage(
+ makeSystemMessage("FrankerFaceZ channel emotes reloaded."));
return Success;
})
- .onError([channelId](NetworkResult result) {
+ .onError([channelId, channel, manualRefresh](NetworkResult result) {
+ auto shared = channel.lock();
+ if (!shared)
+ return;
if (result.status() == 203)
{
// User does not have any FFZ emotes
+ if (manualRefresh)
+ shared->addMessage(makeSystemMessage(
+ "This channel has no FrankerFaceZ channel emotes."));
}
else if (result.status() == NetworkResult::timedoutStatus)
{
// TODO: Auto retry in case of a timeout, with a delay
qDebug() << "Fetching FFZ emotes for channel" << channelId
<< "failed due to timeout";
+ shared->addMessage(
+ makeSystemMessage("Failed to fetch FrankerFaceZ channel "
+ "emotes. (timed out)"));
}
else
{
qDebug() << "Error fetching FFZ emotes for channel" << channelId
<< ", error" << result.status();
+ shared->addMessage(
+ makeSystemMessage("Failed to fetch FrankerFaceZ channel "
+ "emotes. (unknown error)"));
}
})
.execute();
diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp
index 297ac4257..817341c83 100644
--- a/src/providers/ffz/FfzEmotes.hpp
+++ b/src/providers/ffz/FfzEmotes.hpp
@@ -4,6 +4,7 @@
#include "boost/optional.hpp"
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
+#include "providers/twitch/TwitchChannel.hpp"
namespace chatterino {
@@ -23,9 +24,10 @@ public:
boost::optional emote(const EmoteName &name) const;
void loadEmotes();
static void loadChannel(
- const QString &channelId,
+ std::weak_ptr channel, const QString &channelId,
std::function emoteCallback,
- std::function)> modBadgeCallback);
+ std::function)> modBadgeCallback,
+ bool manualRefresh);
private:
Atomic> global_;
diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp
index 0a3019e5c..2618465a5 100644
--- a/src/providers/irc/AbstractIrcServer.cpp
+++ b/src/providers/irc/AbstractIrcServer.cpp
@@ -71,8 +71,30 @@ AbstractIrcServer::AbstractIrcServer()
});
}
+void AbstractIrcServer::initializeIrc()
+{
+ assert(!this->initialized_);
+
+ if (this->hasSeparateWriteConnection())
+ {
+ this->initializeConnectionSignals(this->writeConnection_.get(),
+ ConnectionType::Write);
+ this->initializeConnectionSignals(this->readConnection_.get(),
+ ConnectionType::Read);
+ }
+ else
+ {
+ this->initializeConnectionSignals(this->readConnection_.get(),
+ ConnectionType::Both);
+ }
+
+ this->initialized_ = true;
+}
+
void AbstractIrcServer::connect()
{
+ assert(this->initialized_);
+
this->disconnect();
if (this->hasSeparateWriteConnection())
diff --git a/src/providers/irc/AbstractIrcServer.hpp b/src/providers/irc/AbstractIrcServer.hpp
index 20d782490..1c865ef52 100644
--- a/src/providers/irc/AbstractIrcServer.hpp
+++ b/src/providers/irc/AbstractIrcServer.hpp
@@ -21,6 +21,10 @@ public:
virtual ~AbstractIrcServer() = default;
+ // initializeIrc must be called from the derived class
+ // this allows us to initialize the abstract irc server based on the derived class's parameters
+ void initializeIrc();
+
// connection
void connect();
void disconnect();
@@ -45,8 +49,15 @@ public:
protected:
AbstractIrcServer();
+ // initializeConnectionSignals is called on a connection once in its lifetime.
+ // it can be used to connect signals to your class
+ virtual void initializeConnectionSignals(IrcConnection *connection,
+ ConnectionType type){};
+
+ // initializeConnection is called every time before we try to connect to the irc server
virtual void initializeConnection(IrcConnection *connection,
ConnectionType type) = 0;
+
virtual std::shared_ptr createChannel(
const QString &channelName) = 0;
@@ -83,6 +94,8 @@ private:
// bool autoReconnect_ = false;
pajlada::Signals::SignalHolder connections_;
+
+ bool initialized_{false};
};
} // namespace chatterino
diff --git a/src/providers/irc/Irc2.cpp b/src/providers/irc/Irc2.cpp
index 0a8f2ed60..2f685be63 100644
--- a/src/providers/irc/Irc2.cpp
+++ b/src/providers/irc/Irc2.cpp
@@ -143,7 +143,7 @@ Irc::Irc()
QAbstractTableModel *Irc::newConnectionModel(QObject *parent)
{
auto model = new Model(parent);
- model->init(&this->connections);
+ model->initialize(&this->connections);
return model;
}
@@ -252,7 +252,7 @@ void Irc::load()
{
ids.insert(data.id);
- this->connections.appendItem(data);
+ this->connections.append(data);
}
}
}
diff --git a/src/providers/irc/Irc2.hpp b/src/providers/irc/Irc2.hpp
index de484924a..0cdc60c77 100644
--- a/src/providers/irc/Irc2.hpp
+++ b/src/providers/irc/Irc2.hpp
@@ -41,7 +41,7 @@ public:
static inline void *const noEraseCredentialCaller =
reinterpret_cast(1);
- UnsortedSignalVector connections;
+ SignalVector connections;
QAbstractTableModel *newConnectionModel(QObject *parent);
ChannelPtr getOrAddChannel(int serverId, QString name);
diff --git a/src/providers/irc/IrcMessageBuilder.cpp b/src/providers/irc/IrcMessageBuilder.cpp
new file mode 100644
index 000000000..c7529a5e5
--- /dev/null
+++ b/src/providers/irc/IrcMessageBuilder.cpp
@@ -0,0 +1,93 @@
+#include "providers/irc/IrcMessageBuilder.hpp"
+
+#include "Application.hpp"
+#include "controllers/accounts/AccountController.hpp"
+#include "controllers/ignores/IgnoreController.hpp"
+#include "controllers/ignores/IgnorePhrase.hpp"
+#include "messages/Message.hpp"
+#include "providers/chatterino/ChatterinoBadges.hpp"
+#include "singletons/Emotes.hpp"
+#include "singletons/Resources.hpp"
+#include "singletons/Settings.hpp"
+#include "singletons/Theme.hpp"
+#include "singletons/WindowManager.hpp"
+#include "util/IrcHelpers.hpp"
+#include "widgets/Window.hpp"
+
+namespace chatterino {
+
+IrcMessageBuilder::IrcMessageBuilder(
+ Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
+ const MessageParseArgs &_args)
+ : SharedMessageBuilder(_channel, _ircMessage, _args)
+{
+ this->usernameColor_ = getApp()->themes->messages.textColors.system;
+}
+
+IrcMessageBuilder::IrcMessageBuilder(Channel *_channel,
+ const Communi::IrcMessage *_ircMessage,
+ const MessageParseArgs &_args,
+ QString content, bool isAction)
+ : SharedMessageBuilder(_channel, _ircMessage, _args, content, isAction)
+{
+ assert(false);
+ this->usernameColor_ = getApp()->themes->messages.textColors.system;
+}
+
+MessagePtr IrcMessageBuilder::build()
+{
+ // PARSE
+ this->parse();
+
+ // PUSH ELEMENTS
+ this->appendChannelName();
+
+ this->emplace();
+
+ this->appendUsername();
+
+ // words
+ this->addWords(this->originalMessage_.split(' '));
+
+ this->message().messageText = this->originalMessage_;
+ this->message().searchText = this->message().localizedName + " " +
+ this->userName + ": " + this->originalMessage_;
+
+ // highlights
+ this->parseHighlights();
+
+ // highlighting incoming whispers if requested per setting
+ if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers)
+ {
+ this->message().flags.set(MessageFlag::HighlightedWhisper, true);
+ }
+
+ return this->release();
+}
+
+void IrcMessageBuilder::addWords(const QStringList &words)
+{
+ this->emplace(words.join(' '), MessageElementFlag::Text);
+}
+
+void IrcMessageBuilder::appendUsername()
+{
+ auto app = getApp();
+
+ QString username = this->userName;
+ this->message().loginName = username;
+
+ // The full string that will be rendered in the chat widget
+ QString usernameText = username;
+
+ if (!this->action_)
+ {
+ usernameText += ":";
+ }
+
+ this->emplace(usernameText, MessageElementFlag::Username,
+ this->usernameColor_, FontStyle::ChatMediumBold)
+ ->setLink({Link::UserInfo, this->message().displayName});
+}
+
+} // namespace chatterino
diff --git a/src/providers/irc/IrcMessageBuilder.hpp b/src/providers/irc/IrcMessageBuilder.hpp
new file mode 100644
index 000000000..d6a83da8a
--- /dev/null
+++ b/src/providers/irc/IrcMessageBuilder.hpp
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "common/Aliases.hpp"
+#include "common/Outcome.hpp"
+#include "messages/SharedMessageBuilder.hpp"
+#include "providers/twitch/TwitchBadge.hpp"
+
+#include
+#include
+#include
+
+namespace chatterino {
+
+struct Emote;
+using EmotePtr = std::shared_ptr;
+
+class Channel;
+class TwitchChannel;
+
+class IrcMessageBuilder : public SharedMessageBuilder
+{
+public:
+ IrcMessageBuilder() = delete;
+
+ explicit IrcMessageBuilder(Channel *_channel,
+ const Communi::IrcPrivateMessage *_ircMessage,
+ const MessageParseArgs &_args);
+ explicit IrcMessageBuilder(Channel *_channel,
+ const Communi::IrcMessage *_ircMessage,
+ const MessageParseArgs &_args, QString content,
+ bool isAction);
+
+ MessagePtr build() override;
+
+private:
+ void appendUsername();
+
+ void addWords(const QStringList &words);
+};
+
+} // namespace chatterino
diff --git a/src/providers/irc/IrcServer.cpp b/src/providers/irc/IrcServer.cpp
index dd7505ff9..006e55acb 100644
--- a/src/providers/irc/IrcServer.cpp
+++ b/src/providers/irc/IrcServer.cpp
@@ -4,9 +4,9 @@
#include
#include "messages/Message.hpp"
-#include "messages/MessageBuilder.hpp"
#include "providers/irc/Irc2.hpp"
#include "providers/irc/IrcChannel2.hpp"
+#include "providers/irc/IrcMessageBuilder.hpp"
#include "singletons/Settings.hpp"
#include "util/QObjectRef.hpp"
@@ -15,6 +15,8 @@ namespace chatterino {
IrcServer::IrcServer(const IrcServerData &data)
: data_(new IrcServerData(data))
{
+ this->initializeIrc();
+
this->connect();
}
@@ -51,6 +53,58 @@ const QString &IrcServer::nick()
return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick;
}
+void IrcServer::initializeConnectionSignals(IrcConnection *connection,
+ ConnectionType type)
+{
+ assert(type == Both);
+
+ QObject::connect(
+ connection, &Communi::IrcConnection::socketError, this,
+ [this](QAbstractSocket::SocketError error) {
+ static int index =
+ QAbstractSocket::staticMetaObject.indexOfEnumerator(
+ "SocketError");
+
+ std::lock_guard lock(this->channelMutex);
+
+ for (auto &&weak : this->channels)
+ {
+ if (auto shared = weak.lock())
+ {
+ shared->addMessage(makeSystemMessage(
+ QStringLiteral("Socket error: ") +
+ QAbstractSocket::staticMetaObject.enumerator(index)
+ .valueToKey(error)));
+ }
+ }
+ });
+
+ QObject::connect(connection, &Communi::IrcConnection::nickNameRequired,
+ this, [](const QString &reserved, QString *result) {
+ *result = reserved + (std::rand() % 100);
+ });
+
+ QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
+ this, [this](Communi::IrcNoticeMessage *message) {
+ // XD PAJLADA
+ MessageBuilder builder;
+
+ builder.emplace();
+ builder.emplace(
+ message->nick(), MessageElementFlag::Username);
+ builder.emplace(
+ "-> you:", MessageElementFlag::Username);
+ builder.emplace(message->content(),
+ MessageElementFlag::Text);
+
+ auto msg = builder.release();
+
+ for (auto &&weak : this->channels)
+ if (auto shared = weak.lock())
+ shared->addMessage(msg);
+ });
+}
+
void IrcServer::initializeConnection(IrcConnection *connection,
ConnectionType type)
{
@@ -90,47 +144,6 @@ void IrcServer::initializeConnection(IrcConnection *connection,
this->open(Both);
}
}
-
- QObject::connect(
- connection, &Communi::IrcConnection::socketError, this,
- [this](QAbstractSocket::SocketError error) {
- static int index =
- QAbstractSocket::staticMetaObject.indexOfEnumerator(
- "SocketError");
-
- std::lock_guard lock(this->channelMutex);
-
- for (auto &&weak : this->channels)
- if (auto shared = weak.lock())
- shared->addMessage(makeSystemMessage(
- QStringLiteral("Socket error: ") +
- QAbstractSocket::staticMetaObject.enumerator(index)
- .valueToKey(error)));
- });
-
- QObject::connect(connection, &Communi::IrcConnection::nickNameRequired,
- this, [](const QString &reserved, QString *result) {
- *result = reserved + (std::rand() % 100);
- });
-
- QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
- this, [this](Communi::IrcNoticeMessage *message) {
- MessageBuilder builder;
-
- builder.emplace();
- builder.emplace(
- message->nick(), MessageElementFlag::Username);
- builder.emplace(
- "-> you:", MessageElementFlag::Username);
- builder.emplace