Merge branch 'master' into custom-highlight-color-tabs

This commit is contained in:
pajlada 2020-08-09 05:55:53 -04:00 committed by GitHub
commit 2d93ceed67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
223 changed files with 6441 additions and 2926 deletions

View file

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

247
.CI/dmg-settings.py Normal file
View file

@ -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".'
# ),
# },
# }

View file

@ -7,14 +7,8 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**What should be added?**
<!-- A clear and concise description of the requested feature. -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
**Why should it be added?**
<!-- A clear and concise description of what motivates you to request this change. -->

View file

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

View file

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

17
BUILDING_ON_FREEBSD.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

20
docs/Commands.md Normal file
View file

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

View file

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

47
docs/IMAGEUPLOADER.md Normal file
View file

@ -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 (`;`).<br>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.|
<br>
## 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`|

View file

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

45
docs/Regex.md Normal file
View file

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

@ -1 +1 @@
Subproject commit 1c38746b05d9311e73c8c8acdfdc4d36c9c551be
Subproject commit 6665ccad90461c01b7fe704a98a835953d644156

View file

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

View file

@ -39,7 +39,6 @@ chatterino--TitleLabel {
font-family: "Segoe UI light";
font-size: 24px;
color: #4FC3F7;
margin-top: 16px;
}
chatterino--DescriptionLabel {

View file

@ -28,8 +28,9 @@
<file>buttons/unmod.png</file>
<file>buttons/update.png</file>
<file>buttons/updateError.png</file>
<file>com.chatterino.chatterino.desktop</file>
<file>chatterino.icns</file>
<file>com.chatterino.chatterino.appdata.xml</file>
<file>com.chatterino.chatterino.desktop</file>
<file>contributors.txt</file>
<file>emoji.json</file>
<file>emojidata.txt</file>
@ -50,6 +51,12 @@
<file>licenses/websocketpp.txt</file>
<file>pajaDank.png</file>
<file>qss/settings.qss</file>
<file>scrolling/downScroll.png</file>
<file>scrolling/downScroll.svg</file>
<file>scrolling/neutralScroll.png</file>
<file>scrolling/neutralScroll.svg</file>
<file>scrolling/upScroll.png</file>
<file>scrolling/upScroll.svg</file>
<file>settings/about.svg</file>
<file>settings/aboutlogo.png</file>
<file>settings/accounts.svg</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="downScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="14.417768"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<circle
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3719"
cx="4.2333331"
cy="292.76666"
r="4.2145276" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="4.2333317"
cy="292.76425"
r="0.72627902" />
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4538"
sodipodi:sides="3"
sodipodi:cx="99.21875"
sodipodi:cy="155.44792"
sodipodi:r1="13.229166"
sodipodi:r2="6.614583"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:transform-center-x="5.0649804e-06"
inkscape:transform-center-y="2.2264812"
transform="matrix(0,0.09239709,-0.09239709,0,18.596269,286.19867)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="neutralScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="25.340042"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<g
id="g5271"
transform="matrix(0.07922061,0,0,0.07922061,-4.1782222,281.17363)">
<circle
r="53.199886"
cy="146.33852"
cx="106.17888"
id="path3719"
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.47476631;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<g
transform="matrix(1.7324984,0,0,1.7324984,-77.775866,-107.19273)"
id="g4665">
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.36790693;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="106.17887"
cy="146.32092"
r="5.2916665" />
<path
sodipodi:type="star"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4538"
sodipodi:sides="3"
sodipodi:cx="99.21875"
sodipodi:cy="155.44792"
sodipodi:r1="13.229166"
sodipodi:r2="6.614583"
sodipodi:arg1="0"
sodipodi:arg2="1.0471976"
inkscape:flatsided="true"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:transform-center-x="5.0649804e-06"
inkscape:transform-center-y="2.2264812"
transform="matrix(0,0.67320493,-0.67320493,0,210.82719,98.484167)" />
<path
transform="matrix(0,-0.67320493,-0.67320493,0,210.82719,194.19287)"
inkscape:transform-center-y="-2.2264854"
inkscape:transform-center-x="5.0649804e-06"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="6.614583"
sodipodi:r1="13.229166"
sodipodi:cy="155.44792"
sodipodi:cx="99.21875"
sodipodi:sides="3"
id="path4540"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="32"
height="32"
viewBox="0 0 8.4666665 8.4666669"
version="1.1"
id="svg8"
inkscape:version="0.92.4 5da689c313, 2019-01-14"
sodipodi:docname="upScroll.svg"
inkscape:export-filename="/home/leon/Projects/Chatterino/chatterino2/resources/scrolling/neutralScroll.png"
inkscape:export-xdpi="7.6051788"
inkscape:export-ydpi="7.6051788">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="15.839192"
inkscape:cx="14.417768"
inkscape:cy="15.865661"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="1918"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
units="px"
inkscape:pagecheckerboard="true">
<inkscape:grid
type="xygrid"
id="grid4532" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-288.53332)">
<circle
style="fill:#f6f6f6;fill-opacity:1;stroke:#000000;stroke-width:0.03761128;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3719"
cx="4.2333331"
cy="292.76666"
r="4.2145276" />
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.05049507;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4526"
cx="4.2333317"
cy="292.76425"
r="0.72627902" />
<path
transform="matrix(0,-0.09239709,-0.09239709,0,18.596269,299.33465)"
inkscape:transform-center-y="-2.2264854"
inkscape:transform-center-x="5.0649804e-06"
d="m 112.44792,155.44792 -19.843753,11.4568 v -22.91359 z"
inkscape:randomized="0"
inkscape:rounded="0"
inkscape:flatsided="true"
sodipodi:arg2="1.0471976"
sodipodi:arg1="0"
sodipodi:r2="6.614583"
sodipodi:r1="13.229166"
sodipodi:cy="155.44792"
sodipodi:cx="99.21875"
sodipodi:sides="3"
id="path4540"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.56499994;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="star" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -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<AccountController>())
, commands(&this->emplace<CommandController>())
, highlights(&this->emplace<HighlightController>())
, notifications(&this->emplace<NotificationController>())
, pings(&this->emplace<PingController>())
, ignores(&this->emplace<IgnoreController>())
, taggedUsers(&this->emplace<TaggedUsersController>())
, moderationActions(&this->emplace<ModerationActions>())
, twitch2(&this->emplace<TwitchIrcServer>())
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
, logging(&this->emplace<Logging>())
{
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<TwitchChannel *>(chan.get());
channel->addChannelPointReward(ChannelPointReward(data));
}
else
{
qDebug() << "Couldn't find channel id of point reward";
}
});
this->twitch.pubsub->start();
auto RequestModerationActions = [=]() {

View file

@ -3,6 +3,7 @@
#include <QApplication>
#include <memory>
#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{};

View file

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

View file

@ -64,8 +64,6 @@ public:
struct {
QColor regular;
QColor alternate;
QColor highlighted;
QColor subscription;
// QColor whisper;
} backgrounds;

View file

@ -154,11 +154,6 @@ namespace {
toBeRemoved << info.absoluteFilePath();
}
}
for (auto &&path : toBeRemoved)
{
qDebug() << path << QFile(path).remove();
}
}
} // namespace

View file

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

View file

@ -1,5 +1,4 @@
#include <QPixmap>
#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

View file

@ -34,3 +34,4 @@ QStringAlias(Url);
QStringAlias(Tooltip);
QStringAlias(EmoteId);
QStringAlias(EmoteName);
QStringAlias(EmoteAuthor);

View file

@ -59,6 +59,11 @@ bool Channel::isEmpty() const
return this->name_.isEmpty();
}
bool Channel::hasMessages() const
{
return !this->messages_.empty();
}
LimitedQueueSnapshot<MessagePtr> 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;

View file

@ -75,6 +75,8 @@ public:
void deleteMessage(QString messageID);
void clearMessages();
bool hasMessages() const;
QStringList modList;
// CHANNEL INFO

View file

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

View file

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

62
src/common/IrcColors.hpp Normal file
View file

@ -0,0 +1,62 @@
#pragma once
#include <QColor>
#include <QMap>
namespace chatterino {
// Colors taken from https://modern.ircdocs.horse/formatting.html
static QMap<int, QColor> 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

View file

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

View file

@ -3,6 +3,7 @@
#include "common/NetworkCommon.hpp"
#include "common/NetworkResult.hpp"
#include <QHttpMultiPart>
#include <memory>
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,

View file

@ -4,244 +4,171 @@
#include <QTimer>
#include <boost/noncopyable.hpp>
#include <pajlada/signals/signal.hpp>
#include <shared_mutex>
#include <vector>
#include "debug/AssertInGuiThread.hpp"
namespace chatterino {
template <typename TVectorItem>
struct SignalVectorItemArgs {
const TVectorItem &item;
template <typename T>
struct SignalVectorItemEvent {
const T &item;
int index;
void *caller;
};
template <typename TVectorItem>
class ReadOnlySignalVector : boost::noncopyable
template <typename T>
class SignalVector : boost::noncopyable
{
using VecIt = typename std::vector<TVectorItem>::iterator;
public:
struct Iterator
: public std::iterator<std::input_iterator_tag, TVectorItem> {
Iterator(VecIt &&it, std::shared_mutex &mutex)
: it_(std::move(it))
, lock_(mutex)
, mutex_(mutex)
{
}
pajlada::Signals::Signal<SignalVectorItemEvent<T>> itemInserted;
pajlada::Signals::Signal<SignalVectorItemEvent<T>> 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<std::shared_mutex> lock_;
std::reference_wrapper<std::shared_mutex> mutex_;
};
ReadOnlySignalVector()
SignalVector()
: readOnly_(new std::vector<T>())
{
QObject::connect(&this->itemsChangedTimer_, &QTimer::timeout,
[this] { this->delayedItemsChanged.invoke(); });
this->itemsChangedTimer_.setInterval(100);
this->itemsChangedTimer_.setSingleShot(true);
}
virtual ~ReadOnlySignalVector() = default;
pajlada::Signals::Signal<SignalVectorItemArgs<TVectorItem>> itemInserted;
pajlada::Signals::Signal<SignalVectorItemArgs<TVectorItem>> itemRemoved;
pajlada::Signals::NoArgSignal delayedItemsChanged;
Iterator begin() const
SignalVector(std::function<bool(const T &, const T &)> &&compare)
: SignalVector()
{
return Iterator(
const_cast<std::vector<TVectorItem> &>(this->vector_).begin(),
this->mutex_);
itemCompare_ = std::move(compare);
}
Iterator end() const
virtual bool isSorted() const
{
return Iterator(
const_cast<std::vector<TVectorItem> &>(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<const std::vector<T>> readOnly()
{
std::shared_lock lock(this->mutex_);
return this->vector_.empty();
return this->readOnly_;
}
const std::vector<TVectorItem> &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<T> args{item, index, caller};
this->itemInserted.invoke(args);
this->itemsChanged_();
return index;
}
std::vector<TVectorItem> 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<T> args{item, index, caller};
this->itemRemoved.invoke(args);
this->itemsChanged_();
}
const std::vector<T> &raw() const
{
assertInGuiThread();
return this->items_;
}
[[deprecated]] std::vector<T> 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<const std::vector<T>>(this->items_);
}
virtual bool isSorted() const = 0;
protected:
std::vector<TVectorItem> vector_;
std::vector<T> items_;
std::shared_ptr<const std::vector<T>> readOnly_;
QTimer itemsChangedTimer_;
mutable std::shared_mutex mutex_;
};
template <typename TVectorItem>
class BaseSignalVector : public ReadOnlySignalVector<TVectorItem>
{
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<TVectorItem> 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 <typename TVectorItem>
class UnsortedSignalVector : public BaseSignalVector<TVectorItem>
{
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<TVectorItem> args{item, index, caller};
this->itemInserted.invoke(args);
this->invokeDelayedItemsChanged();
return index;
}
virtual bool isSorted() const override
{
return false;
}
};
template <typename TVectorItem, typename Compare>
class SortedSignalVector : public BaseSignalVector<TVectorItem>
{
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<TVectorItem> args{item, index, caller};
this->itemInserted.invoke(args);
this->invokeDelayedItemsChanged();
return index;
}
virtual bool isSorted() const override
{
return true;
}
std::function<bool(const T &, const T &)> itemCompare_;
};
} // namespace chatterino

View file

@ -25,11 +25,11 @@ public:
}
}
void init(BaseSignalVector<TVectorItem> *vec)
void initialize(SignalVector<TVectorItem> *vec)
{
this->vector_ = vec;
auto insert = [this](const SignalVectorItemArgs<TVectorItem> &args) {
auto insert = [this](const SignalVectorItemEvent<TVectorItem> &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<TVectorItem> args{item, i++, 0};
SignalVectorItemEvent<TVectorItem> args{item, i++, 0};
insert(args);
}
@ -89,6 +89,12 @@ public:
this->afterInit();
}
SignalVectorModel<TVectorItem> *initialized(SignalVector<TVectorItem> *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<QMap<int, QVariant>> headerData_;
BaseSignalVector<TVectorItem> *vector_;
SignalVector<TVectorItem> *vector_;
std::vector<Row> rows_;
int columnCount_;

View file

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

View file

@ -76,6 +76,8 @@ public:
std::pair<Iterator, bool> insert(const QString &value);
std::pair<Iterator, bool> insert(QString &&value);
bool contains(const QString &value) const;
private:
void insertPrefix(const QString &string);

View file

@ -7,20 +7,20 @@
namespace chatterino {
AccountController::AccountController()
: accounts_(SharedPtrElementLess<Account>{})
{
this->twitch.accounts.itemInserted.connect([this](const auto &args) {
this->accounts_.insertItem(
std::dynamic_pointer_cast<Account>(args.item));
this->accounts_.insert(std::dynamic_pointer_cast<Account>(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;
}

View file

@ -27,8 +27,7 @@ public:
TwitchAccountManager twitch;
private:
SortedSignalVector<std::shared_ptr<Account>, SharedPtrElementLess<Account>>
accounts_;
SignalVector<std::shared_ptr<Account>> accounts_;
};
} // namespace chatterino

View file

@ -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 <QApplication>
#include <QFile>
#include <QRegularExpression>
#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<std::mutex> 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<Channel> 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 "";

View file

@ -22,7 +22,7 @@ class CommandModel;
class CommandController final : public Singleton
{
public:
UnsortedSignalVector<Command> items_;
SignalVector<Command> items_;
QString execCommand(const QString &text, std::shared_ptr<Channel> channel,
bool dryRun);
@ -39,8 +39,6 @@ private:
QMap<QString, Command> commandsMap_;
int maxSpaces_ = 0;
std::mutex mutex_;
std::shared_ptr<pajlada::Settings::SettingManager> sm_;
// Because the setting manager is not initialized until the initialize
// function is called (and not in the constructor), we have to

View file

@ -1,5 +1,7 @@
#include "CommandModel.hpp"
#include "util/StandardItemHelper.hpp"
namespace chatterino {
// commandmodel
@ -20,12 +22,8 @@ Command CommandModel::getItemFromRow(std::vector<QStandardItem *> &row,
void CommandModel::getRowFromItem(const Command &item,
std::vector<QStandardItem *> &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

View file

@ -11,9 +11,9 @@ class HighlightController;
class HighlightBlacklistModel : public SignalVectorModel<HighlightBlacklistUser>
{
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<QStandardItem *> &row) override;
friend class HighlightController;
};
} // namespace chatterino

View file

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

View file

@ -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<const Message>;
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<HighlightPhrase> phrases;
UnsortedSignalVector<HighlightBlacklistUser> blacklistedUsers;
UnsortedSignalVector<HighlightPhrase> 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<std::vector<HighlightPhrase>> highlightsSetting_ = {
"/highlighting/highlights"};
ChatterinoSetting<std::vector<HighlightBlacklistUser>> blacklistSetting_ = {
"/highlighting/blacklist"};
ChatterinoSetting<std::vector<HighlightPhrase>> userSetting_ = {
"/highlighting/users"};
};
} // namespace chatterino

View file

@ -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<QStandardItem *> 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<QStandardItem *> 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<QStandardItem *> &row,
@ -128,7 +157,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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<QStandardItem *> &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 &>(ColorProvider::instance())
.updateColor(ColorType::RedeemedHighlight,
QColor(colorName));
}
}
}
break;
}
getApp()->windows->forceLayoutChannelViews();
}
} // namespace chatterino

View file

@ -7,13 +7,11 @@
namespace chatterino {
class HighlightController;
class HighlightModel : public SignalVectorModel<HighlightPhrase>
{
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<QStandardItem *> &row,
int column, const QVariant &value, int role,
int rowIndex) override;
friend class HighlightController;
};
} // namespace chatterino

View file

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

View file

@ -70,6 +70,14 @@ public:
const QUrl &getSoundUrl() const;
const std::shared_ptr<QColor> 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::HighlightPhrase> {
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,

View file

@ -11,6 +11,7 @@ class HighlightController;
class UserHighlightModel : public SignalVectorModel<HighlightPhrase>
{
public:
explicit UserHighlightModel(QObject *parent);
protected:
@ -21,8 +22,6 @@ protected:
virtual void getRowFromItem(const HighlightPhrase &item,
std::vector<QStandardItem *> &row) override;
friend class HighlightController;
};
} // namespace chatterino

View file

@ -1,33 +0,0 @@
#include "controllers/ignores/IgnoreController.hpp"
#include "Application.hpp"
#include "controllers/ignores/IgnoreModel.hpp"
#include <cassert>
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

View file

@ -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<IgnorePhrase> phrases;
IgnoreModel *createModel(QObject *parent);
private:
bool initialized_ = false;
ChatterinoSetting<std::vector<IgnorePhrase>> ignoresSetting_ = {
"/ignore/phrases"};
};
} // namespace chatterino

View file

@ -7,10 +7,9 @@
namespace chatterino {
class IgnoreController;
class IgnoreModel : public SignalVectorModel<IgnorePhrase>
{
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<QStandardItem *> &row) override;
friend class IgnoreController;
};
} // namespace chatterino

View file

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

View file

@ -25,10 +25,11 @@ public:
const QString &getAction() const;
private:
boost::optional<ImagePtr> image_;
mutable boost::optional<ImagePtr> image_;
QString line1_;
QString line2_;
QString action_;
int imageToLoad_{};
};
} // namespace chatterino

View file

@ -7,8 +7,6 @@
namespace chatterino {
class ModerationActions;
class ModerationActionModel : public SignalVectorModel<ModerationAction>
{
public:
@ -25,8 +23,6 @@ protected:
std::vector<QStandardItem *> &row) override;
friend class HighlightController;
friend class ModerationActions;
};
} // namespace chatterino

View file

@ -1,42 +0,0 @@
#include "ModerationActions.hpp"
#include "Application.hpp"
#include "controllers/moderationactions/ModerationActionModel.hpp"
#include "singletons/Settings.hpp"
#include <QRegularExpression>
namespace chatterino {
ModerationActions::ModerationActions()
{
}
void ModerationActions::initialize(Settings &settings, Paths &paths)
{
assert(!this->initialized_);
this->initialized_ = true;
this->setting_ =
std::make_unique<ChatterinoSetting<std::vector<ModerationAction>>>(
"/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

View file

@ -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<ModerationAction> items;
ModerationActionModel *createModel(QObject *parent);
private:
std::unique_ptr<ChatterinoSetting<std::vector<ModerationAction>>> setting_;
bool initialized_ = false;
};
} // namespace chatterino

View file

@ -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<int>::size_type i = 0;
i != channelMap[p].getVector().size(); i++)
for (std::vector<int>::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<int>::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)

View file

@ -30,9 +30,9 @@ public:
void playSound();
UnsortedSignalVector<QString> getVector(Platform p);
SignalVector<QString> getVector(Platform p);
std::map<Platform, UnsortedSignalVector<QString>> channelMap;
std::map<Platform, SignalVector<QString>> 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<QString> fakeTwitchChannels;
QTimer *liveStatusTimer_;

View file

@ -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<QString>(1, parent)
{
}
// turn a vector item into a model row
QString MutedChannelModel::getItemFromRow(std::vector<QStandardItem *> &row,
const QString &original)
{
return QString(row[0]->data(Qt::DisplayRole).toString());
}
// turn a model
void MutedChannelModel::getRowFromItem(const QString &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item);
}
} // namespace chatterino

View file

@ -7,11 +7,11 @@
namespace chatterino {
class PingController;
class MutedChannelController;
class PingModel : public SignalVectorModel<QString>
class MutedChannelModel : public SignalVectorModel<QString>
{
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<QStandardItem *> &row) override;
friend class PingController;
};
} // namespace chatterino

View file

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

View file

@ -1,36 +0,0 @@
#pragma once
#include <QObject>
#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<QString> channelVector;
ChatterinoSetting<std::vector<QString>> pingSetting_ = {"/pings/muted"};
};
} // namespace chatterino

View file

@ -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<QString>(1, parent)
{
}
// turn a vector item into a model row
QString PingModel::getItemFromRow(std::vector<QStandardItem *> &row,
const QString &original)
{
return QString(row[0]->data(Qt::DisplayRole).toString());
}
// turn a model
void PingModel::getRowFromItem(const QString &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item);
}
} // namespace chatterino

View file

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

View file

@ -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<TaggedUser, std::less<TaggedUser>> users;
TaggedUsersModel *createModel(QObject *parent = nullptr);
};
} // namespace chatterino

View file

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

View file

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

View file

@ -35,6 +35,7 @@ namespace detail {
boost::optional<QPixmap> first() const;
private:
void processOffset();
QVector<Frame<QPixmap>> items_;
int index_{0};
int durationOffset_{0};

View file

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

View file

@ -19,4 +19,9 @@ bool Link::isValid() const
return this->type != None;
}
bool Link::isUrl() const
{
return this->type == Url;
}
} // namespace chatterino

View file

@ -28,6 +28,7 @@ public:
QString value;
bool isValid() const;
bool isUrl() const;
};
} // namespace chatterino

View file

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

View file

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

View file

@ -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<MessagePtr, MessagePtr> 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<TimestampElement>();
this->emplace<TimestampElement>(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<TimestampElement>();
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<TimestampElement>();
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<TimestampElement>();
this->emplace<TextElement>(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<TimestampElement>();
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<TextElement>(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<TextElement>(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<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
}
} // namespace chatterino

View file

@ -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<MessagePtr, MessagePtr> 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> message_;
};
} // namespace chatterino

View file

@ -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<Segment> 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<Segment> segments,
int width, bool hasTrailingSpace) {
std::vector<PajSegment> 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<TextLayoutElement *>(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<Segment> 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<Segment> 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

View file

@ -4,6 +4,7 @@
#include "messages/Link.hpp"
#include "messages/MessageColor.hpp"
#include "singletons/Fonts.hpp"
#include "src/messages/ImageSet.hpp"
#include <QRect>
#include <QString>
@ -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<Segment> segments;
};
std::vector<Word> 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

View file

@ -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<HighlightPhrase> 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<EmoteElement>(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<TextElement>(string, MessageElementFlag::BoldUsername,
textColor, FontStyle::ChatMediumBold);
this->emplace<TextElement>(
string, MessageElementFlag::NonBoldUsername, textColor);
}
else
{
this->emplace<TextElement>(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<TextElement>(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

View file

@ -0,0 +1,69 @@
#include "messages/MessageBuilder.hpp"
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include <IrcMessage>
#include <QColor>
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

View file

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

View file

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

View file

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

View file

@ -395,4 +395,41 @@ int TextIconLayoutElement::getXFromIndex(int index)
}
}
//
// TEXT
//
MultiColorTextLayoutElement::MultiColorTextLayoutElement(
MessageElement &_creator, QString &_text, const QSize &_size,
std::vector<PajSegment> 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

View file

@ -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<PajSegment> segments,
FontStyle style_, float scale_);
protected:
void paint(QPainter &painter) override;
private:
std::vector<PajSegment> segments_;
};
} // namespace chatterino

View file

@ -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<void(QString, Link)> successCallback)
std::function<void(QString, Link, ImagePtr)> 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();
// });

View file

@ -3,6 +3,7 @@
#include <QString>
#include <functional>
#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<void(QString, Link)> callback);
static void getLinkInfo(
const QString url, QObject *caller,
std::function<void(QString, Link, ImagePtr)> callback);
};
} // namespace chatterino

View file

@ -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<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
std::pair<Outcome, EmoteMap> 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 + "<br />Channel BetterTTV Emote"},
Tooltip{name.string + "<br>Channel BetterTTV Emote" +
((author.string.isEmpty())
? "<br>By: " + userName.toUtf8()
: "<br>By: " + author.string)},
Url{emoteLinkFormat.arg(id.string)},
});
@ -138,17 +147,51 @@ void BttvEmotes::loadEmotes()
.execute();
}
void BttvEmotes::loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback)
void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
const QString &channelId, const QString &userName,
std::function<void(EmoteMap &&)> 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();
}

View file

@ -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<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
static void loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback);
static void loadChannel(std::weak_ptr<Channel> channel,
const QString &channelId, const QString &userName,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;

View file

@ -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<QColor> 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<QColor>(backgrounds.highlighted)});
std::make_shared<QColor>(
HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
}
customColor = getSettings()->subHighlightColor;
@ -90,7 +89,7 @@ void ColorProvider::initTypeColorMap()
{
this->typeColorMap_.insert(
{ColorType::Subscription,
std::make_shared<QColor>(backgrounds.subscription)});
std::make_shared<QColor>(HighlightPhrase::FALLBACK_SUB_COLOR)});
}
customColor = getSettings()->whisperHighlightColor;
@ -103,22 +102,41 @@ void ColorProvider::initTypeColorMap()
{
this->typeColorMap_.insert(
{ColorType::Whisper,
std::make_shared<QColor>(backgrounds.highlighted)});
std::make_shared<QColor>(
HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR)});
}
customColor = getSettings()->redeemedHighlightColor;
if (QColor(customColor).isValid())
{
this->typeColorMap_.insert({ColorType::RedeemedHighlight,
std::make_shared<QColor>(customColor)});
}
else
{
this->typeColorMap_.insert(
{ColorType::RedeemedHighlight,
std::make_shared<QColor>(
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

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