Merge remote-tracking branch 'main_repo/master' into git_is_pepega

This commit is contained in:
Mm2PL
2020-04-18 13:39:01 +02:00
155 changed files with 3182 additions and 2037 deletions
+10 -10
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
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".'
# ),
# },
# }
+2 -4
View File
@@ -25,9 +25,7 @@ jobs:
submodules: true
- name: Install Qt
uses: jurplel/install-qt-action@v2
with:
modules: qtwebengine
uses: jurplel/install-qt-action@v1
# WINDOWS
- name: Cache conan
@@ -79,7 +77,7 @@ jobs:
# LINUX
- name: Install dependencies (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get -y install libssl-dev libboost-dev libboost-system-dev libboost-filesystem-dev libpulse-dev libxkbcommon-x11-0 libgstreamer-plugins-base1.0-0
run: sudo apt-get update && sudo apt-get -y install libssl-dev libboost-dev libboost-system-dev libboost-filesystem-dev libpulse-dev libxkbcommon-x11-0 libgstreamer-plugins-base1.0-0 build-essential libgl1-mesa-dev
- name: Build (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
+8
View File
@@ -0,0 +1,8 @@
# Changelog
## Unversioned
- Minor: Emotes in the emote popup are now sorted in the same order as the tab completion (#1549)
- 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)
- Settings open faster
- Dev: Fully remove Twitch Chatroom support
View File
+15 -17
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 \
@@ -178,15 +173,14 @@ SOURCES += \
src/providers/irc/IrcConnection2.cpp \
src/providers/irc/IrcServer.cpp \
src/providers/LinkResolver.cpp \
src/providers/twitch/ChatroomChannel.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 \
@@ -328,7 +322,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 \
@@ -337,13 +330,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 \
@@ -383,16 +373,15 @@ HEADERS += \
src/providers/irc/IrcConnection2.hpp \
src/providers/irc/IrcServer.hpp \
src/providers/LinkResolver.hpp \
src/providers/twitch/ChatroomChannel.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 \
@@ -437,6 +426,7 @@ 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 \
@@ -564,3 +554,11 @@ git_hash = $$str_member($$git_commit, 0, 8)
DEFINES += CHATTERINO_GIT_COMMIT=\\\"$$git_commit\\\"
DEFINES += CHATTERINO_GIT_RELEASE=\\\"$$git_release\\\"
DEFINES += CHATTERINO_GIT_HASH=\\\"$$git_hash\\\"
CONFIG(debug, debug|release) {
message("Building Chatterino2 DEBUG")
} else {
message("Building Chatterino2 RELEASE")
}
message("Injected git values: $$git_commit ($$git_release) $$git_hash")
+1
View File
@@ -29,6 +29,7 @@ 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
# If you are a contributor add yourself above this line
Defman21 | https://github.com/Defman21 | | Documentation
-1
View File
@@ -39,7 +39,6 @@ chatterino--TitleLabel {
font-family: "Segoe UI light";
font-size: 24px;
color: #4FC3F7;
margin-top: 16px;
}
chatterino--DescriptionLabel {
+8 -1
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

+96
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

+122
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

+96
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

+7 -21
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"
@@ -54,16 +50,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 +105,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 +117,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 +150,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 =
+1 -10
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{};
-15
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);
-2
View File
@@ -64,8 +64,6 @@ public:
struct {
QColor regular;
QColor alternate;
QColor highlighted;
QColor subscription;
// QColor whisper;
} backgrounds;
-5
View File
@@ -154,11 +154,6 @@ namespace {
toBeRemoved << info.absoluteFilePath();
}
}
for (auto &&path : toBeRemoved)
{
qDebug() << path << QFile(path).remove();
}
}
} // namespace
+4 -1
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
+6 -2
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
+13 -8
View File
@@ -38,13 +38,7 @@ bool CompletionModel::TaggedString::operator<(const TaggedString &that) const
return this->isEmote();
}
// try comparing insensitively, if they are the same then senstively
// (fixes order of LuL and LUL)
int k = QString::compare(this->string, that.string, Qt::CaseInsensitive);
if (k == 0)
return this->string > that.string;
return k < 0;
return CompletionModel::compareStrings(this->string, that.string);
}
//
@@ -133,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)))
@@ -191,4 +185,15 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
}
}
bool CompletionModel::compareStrings(const QString &a, const QString &b)
{
// try comparing insensitively, if they are the same then senstively
// (fixes order of LuL and LUL)
int k = QString::compare(a, b, Qt::CaseInsensitive);
if (k == 0)
return a > b;
return k < 0;
}
} // namespace chatterino
+2
View File
@@ -49,6 +49,8 @@ public:
void refresh(const QString &prefix, bool isFirstWord = false);
static bool compareStrings(const QString &a, const QString &b);
private:
TaggedString createUser(const QString &str);
+118 -191
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
+75 -9
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_;
@@ -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;
}
@@ -27,8 +27,7 @@ public:
TwitchAccountManager twitch;
private:
SortedSignalVector<std::shared_ptr<Account>, SharedPtrElementLess<Account>>
accounts_;
SignalVector<std::shared_ptr<Account>> accounts_;
};
} // namespace chatterino
+20 -24
View File
@@ -8,9 +8,9 @@
#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"
@@ -212,7 +212,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 +222,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 +234,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 +245,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 +364,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,18 +399,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->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 "";
@@ -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
+4 -6
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
@@ -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
@@ -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
@@ -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
+72 -16
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,37 @@ 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 Bits",
Qt::DisplayRole);
setBoolItem(redeemedRow[Column::FlashTaskbar],
getSettings()->enableRedeemedHighlightTaskbar.getValue(), true,
false);
setBoolItem(redeemedRow[Column::PlaySound],
getSettings()->enableRedeemedHighlightSound.getValue(), true,
false);
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 +155,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 +164,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 +180,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 +190,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 +206,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 +216,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 +241,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 +251,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 +268,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
@@ -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
@@ -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_,
@@ -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,
@@ -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
@@ -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
@@ -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
+1 -4
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
@@ -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_;
}
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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_;
@@ -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
@@ -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
-70
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
-36
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
-28
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
@@ -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
@@ -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
+5
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"
@@ -36,6 +38,9 @@ int main(int argc, char **argv)
}
else
{
Helix::initialize();
Kraken::initialize();
Paths *paths{};
try
+13 -10
View File
@@ -41,6 +41,14 @@ 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; });
this->durationOffset_ = std::min<int>(
int(getApp()->emotes->gifTimer.position() % totalLength), 60000);
this->processOffset();
}
Frames::~Frames()
@@ -58,17 +66,16 @@ namespace detail {
void Frames::advance()
{
this->durationOffset_ += GIF_FRAME_LENGTH;
this->durationOffset_ += gifFrameLength;
this->processOffset();
}
void Frames::processOffset()
{
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 +226,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;
}
+1
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};
-2
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)
{
+6
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();
}
+1
View File
@@ -34,6 +34,7 @@ enum class MessageFlag : uint32_t {
HighlightedWhisper = (1 << 17),
Debug = (1 << 18),
Similar = (1 << 19),
RedeemedHighlight = (1 << 20),
};
using MessageFlags = FlagsEnum<MessageFlag>;
+58 -23
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"
@@ -170,9 +171,12 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
this->message().searchText = text;
}
// 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,43 +185,74 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
QString text;
if (action.isBan())
if (action.target.id == current->getUserId())
{
if (action.reason.isEmpty())
text.append("You were ");
if (action.isBan())
{
text = QString("%1 banned %2.") //
.arg(action.source.name)
.arg(action.target.name);
text.append("banned");
}
else
{
text = QString("%1 banned %2: \"%3\".") //
.arg(action.source.name)
.arg(action.target.name)
.arg(action.reason);
text.append(
QString("timed out for %1").arg(formatTime(action.duration)));
}
if (!action.source.name.isEmpty())
{
text.append(" by ");
text.append(action.source.name);
}
if (action.reason.isEmpty())
{
text.append(".");
}
else
{
text.append(QString(": \"%1\".").arg(action.reason));
}
}
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));
if (action.reason.isEmpty())
{
text = QString("%1 banned %2.") //
.arg(action.source.name)
.arg(action.target.name);
}
else
{
text = QString("%1 banned %2: \"%3\".") //
.arg(action.source.name)
.arg(action.target.name)
.arg(action.reason);
}
}
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);
}
if (action.reason.isEmpty())
{
text = QString("%1 timed out %2 for %3.") //
.arg(action.source.name)
.arg(action.target.name)
.arg(formatTime(action.duration));
}
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);
}
if (count > 1)
{
text.append(QString(" (%1 times)").arg(count));
if (count > 1)
{
text.append(QString(" (%1 times)").arg(count));
}
}
}
+31 -20
View File
@@ -1,7 +1,6 @@
#include "messages/MessageElement.hpp"
#include "Application.hpp"
#include "controllers/moderationactions/ModerationActions.hpp"
#include "debug/Benchmark.hpp"
#include "messages/Emote.hpp"
#include "messages/layouts/MessageLayoutContainer.hpp"
@@ -165,21 +164,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 +185,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 +194,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)
@@ -374,8 +385,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())
{
+14 -11
View File
@@ -223,17 +223,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 +233,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
{
+9
View File
@@ -263,6 +263,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 +297,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");
+33 -15
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
+6 -1
View File
@@ -2,7 +2,12 @@
namespace chatterino {
enum class ColorType { SelfHighlight, Subscription, Whisper };
enum class ColorType {
SelfHighlight,
Subscription,
Whisper,
RedeemedHighlight
};
class ColorProvider
{
+1 -1
View File
@@ -102,7 +102,7 @@ namespace {
modBadge = std::make_shared<Emote>(Emote{
{""},
modBadgeImageSet,
Tooltip{"Twitch Channel Moderator"},
Tooltip{"Moderator"},
modBadge1x,
});
}
+22
View File
@@ -71,8 +71,30 @@ AbstractIrcServer::AbstractIrcServer()
});
}
void AbstractIrcServer::initializeIrc()
{
assert(!this->initialized_);
if (this->hasSeparateWriteConnection())
{
this->initializeConnectionSignals(this->writeConnection_.get(),
ConnectionType::Write);
this->initializeConnectionSignals(this->readConnection_.get(),
ConnectionType::Read);
}
else
{
this->initializeConnectionSignals(this->readConnection_.get(),
ConnectionType::Both);
}
this->initialized_ = true;
}
void AbstractIrcServer::connect()
{
assert(this->initialized_);
this->disconnect();
if (this->hasSeparateWriteConnection())
+13
View File
@@ -21,6 +21,10 @@ public:
virtual ~AbstractIrcServer() = default;
// initializeIrc must be called from the derived class
// this allows us to initialize the abstract irc server based on the derived class's parameters
void initializeIrc();
// connection
void connect();
void disconnect();
@@ -45,8 +49,15 @@ public:
protected:
AbstractIrcServer();
// initializeConnectionSignals is called on a connection once in its lifetime.
// it can be used to connect signals to your class
virtual void initializeConnectionSignals(IrcConnection *connection,
ConnectionType type){};
// initializeConnection is called every time before we try to connect to the irc server
virtual void initializeConnection(IrcConnection *connection,
ConnectionType type) = 0;
virtual std::shared_ptr<Channel> createChannel(
const QString &channelName) = 0;
@@ -83,6 +94,8 @@ private:
// bool autoReconnect_ = false;
pajlada::Signals::SignalHolder connections_;
bool initialized_{false};
};
} // namespace chatterino
+2 -2
View File
@@ -143,7 +143,7 @@ Irc::Irc()
QAbstractTableModel *Irc::newConnectionModel(QObject *parent)
{
auto model = new Model(parent);
model->init(&this->connections);
model->initialize(&this->connections);
return model;
}
@@ -252,7 +252,7 @@ void Irc::load()
{
ids.insert(data.id);
this->connections.appendItem(data);
this->connections.append(data);
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public:
static inline void *const noEraseCredentialCaller =
reinterpret_cast<void *>(1);
UnsortedSignalVector<IrcServerData> connections;
SignalVector<IrcServerData> connections;
QAbstractTableModel *newConnectionModel(QObject *parent);
ChannelPtr getOrAddChannel(int serverId, QString name);
+53 -41
View File
@@ -15,6 +15,8 @@ namespace chatterino {
IrcServer::IrcServer(const IrcServerData &data)
: data_(new IrcServerData(data))
{
this->initializeIrc();
this->connect();
}
@@ -51,6 +53,57 @@ const QString &IrcServer::nick()
return this->data_->nick.isEmpty() ? this->data_->user : this->data_->nick;
}
void IrcServer::initializeConnectionSignals(IrcConnection *connection,
ConnectionType type)
{
assert(type == Both);
QObject::connect(
connection, &Communi::IrcConnection::socketError, this,
[this](QAbstractSocket::SocketError error) {
static int index =
QAbstractSocket::staticMetaObject.indexOfEnumerator(
"SocketError");
std::lock_guard lock(this->channelMutex);
for (auto &&weak : this->channels)
{
if (auto shared = weak.lock())
{
shared->addMessage(makeSystemMessage(
QStringLiteral("Socket error: ") +
QAbstractSocket::staticMetaObject.enumerator(index)
.valueToKey(error)));
}
}
});
QObject::connect(connection, &Communi::IrcConnection::nickNameRequired,
this, [](const QString &reserved, QString *result) {
*result = reserved + (std::rand() % 100);
});
QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
this, [this](Communi::IrcNoticeMessage *message) {
MessageBuilder builder;
builder.emplace<TimestampElement>();
builder.emplace<TextElement>(
message->nick(), MessageElementFlag::Username);
builder.emplace<TextElement>(
"-> you:", MessageElementFlag::Username);
builder.emplace<TextElement>(message->content(),
MessageElementFlag::Text);
auto msg = builder.release();
for (auto &&weak : this->channels)
if (auto shared = weak.lock())
shared->addMessage(msg);
});
}
void IrcServer::initializeConnection(IrcConnection *connection,
ConnectionType type)
{
@@ -90,47 +143,6 @@ void IrcServer::initializeConnection(IrcConnection *connection,
this->open(Both);
}
}
QObject::connect(
connection, &Communi::IrcConnection::socketError, this,
[this](QAbstractSocket::SocketError error) {
static int index =
QAbstractSocket::staticMetaObject.indexOfEnumerator(
"SocketError");
std::lock_guard lock(this->channelMutex);
for (auto &&weak : this->channels)
if (auto shared = weak.lock())
shared->addMessage(makeSystemMessage(
QStringLiteral("Socket error: ") +
QAbstractSocket::staticMetaObject.enumerator(index)
.valueToKey(error)));
});
QObject::connect(connection, &Communi::IrcConnection::nickNameRequired,
this, [](const QString &reserved, QString *result) {
*result = reserved + (std::rand() % 100);
});
QObject::connect(connection, &Communi::IrcConnection::noticeMessageReceived,
this, [this](Communi::IrcNoticeMessage *message) {
MessageBuilder builder;
builder.emplace<TimestampElement>();
builder.emplace<TextElement>(
message->nick(), MessageElementFlag::Username);
builder.emplace<TextElement>(
"-> you:", MessageElementFlag::Username);
builder.emplace<TextElement>(message->content(),
MessageElementFlag::Text);
auto msg = builder.release();
for (auto &&weak : this->channels)
if (auto shared = weak.lock())
shared->addMessage(msg);
});
}
std::shared_ptr<Channel> IrcServer::createChannel(const QString &channelName)
+2
View File
@@ -21,6 +21,8 @@ public:
// AbstractIrcServer interface
protected:
void initializeConnectionSignals(IrcConnection *connection,
ConnectionType type) override;
void initializeConnection(IrcConnection *connection,
ConnectionType type) override;
std::shared_ptr<Channel> createChannel(const QString &channelName) override;
-67
View File
@@ -1,67 +0,0 @@
#include "ChatroomChannel.hpp"
#include <QDebug>
#include "TwitchApi.hpp"
#include "common/Common.hpp"
#include "messages/Emote.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/LoadBttvChannelEmote.hpp"
#include "singletons/Emotes.hpp"
namespace chatterino {
ChatroomChannel::ChatroomChannel(const QString &channelName,
TwitchBadges &globalTwitchBadges,
BttvEmotes &globalBttv, FfzEmotes &globalFfz)
: TwitchChannel(channelName, globalTwitchBadges, globalBttv, globalFfz)
{
auto listRef = channelName.splitRef(":");
if (listRef.size() > 2)
{
this->chatroomOwnerId = listRef[1].toString();
}
}
void ChatroomChannel::refreshBTTVChannelEmotes()
{
if (this->chatroomOwnerId.isEmpty())
{
return;
}
TwitchApi::findUserName(
this->chatroomOwnerId,
[this, weak = weakOf<Channel>(this)](QString username) {
BttvEmotes::loadChannel(username, [this, weak](auto &&emoteMap) {
if (auto shared = weak.lock())
this->bttvEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
});
if (auto shared = weak.lock())
{
this->chatroomOwnerName = username;
}
});
}
void ChatroomChannel::refreshFFZChannelEmotes()
{
if (this->chatroomOwnerId.isEmpty())
{
return;
}
FfzEmotes::loadChannel(
this->chatroomOwnerId,
[this](auto &&emoteMap) {
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
},
[this](auto &&modBadge) {
this->ffzCustomModBadge_.set(std::move(modBadge));
});
}
const QString &ChatroomChannel::getDisplayName() const
{
return this->chatroomOwnerName;
}
} // namespace chatterino
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include "TwitchChannel.hpp"
#include <QString>
#include <atomic>
namespace chatterino {
class ChatroomChannel : public TwitchChannel
{
protected:
explicit ChatroomChannel(const QString &channelName,
TwitchBadges &globalTwitchBadges,
BttvEmotes &globalBttv, FfzEmotes &globalFfz);
virtual void refreshBTTVChannelEmotes() override;
virtual void refreshFFZChannelEmotes() override;
virtual const QString &getDisplayName() const override;
QString chatroomOwnerId;
QString chatroomOwnerName;
friend class TwitchIrcServer;
friend class TwitchMessageBuilder;
friend class IrcMessageHandler;
};
} // namespace chatterino
@@ -2,7 +2,6 @@
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "messages/LimitedQueue.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/TwitchAccountManager.hpp"
@@ -241,7 +240,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
if (highlighted)
{
server.mentionsChannel->addMessage(msg);
getApp()->highlights->addHighlight(msg);
}
}
@@ -453,7 +451,6 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
{
auto app = getApp();
qDebug() << "Received whisper!";
MessageParseArgs args;
args.isReceivedWhisper = true;
@@ -1,85 +0,0 @@
#include "providers/twitch/PartialTwitchUser.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include <QJsonArray>
#include <cassert>
namespace chatterino {
PartialTwitchUser PartialTwitchUser::byName(const QString &username)
{
PartialTwitchUser user;
user.username_ = username;
return user;
}
PartialTwitchUser PartialTwitchUser::byId(const QString &id)
{
PartialTwitchUser user;
user.id_ = id;
return user;
}
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
const QObject *caller)
{
getId(
successCallback, [] {}, caller);
}
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
std::function<void()> failureCallback,
const QObject *caller)
{
assert(!this->username_.isEmpty());
NetworkRequest("https://api.twitch.tv/kraken/users?login=" +
this->username_)
.caller(caller)
.authorizeTwitchV5(getDefaultClientID())
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
if (!root.value("users").isArray())
{
qDebug()
<< "API Error while getting user id, users is not an array";
failureCallback();
return Failure;
}
auto users = root.value("users").toArray();
if (users.size() != 1)
{
qDebug() << "API Error while getting user id, users array size "
"is not 1";
failureCallback();
return Failure;
}
if (!users[0].isObject())
{
qDebug() << "API Error while getting user id, first user is "
"not an object";
failureCallback();
return Failure;
}
auto firstUser = users[0].toObject();
auto id = firstUser.value("_id");
if (!id.isString())
{
qDebug() << "API Error: while getting user id, first user "
"object `_id` key is not a string";
failureCallback();
return Failure;
}
successCallback(id.toString());
return Success;
})
.execute();
}
} // namespace chatterino
@@ -1,30 +0,0 @@
#pragma once
#include <QObject>
#include <QString>
#include <functional>
namespace chatterino {
// Experimental class to test a method of calling APIs on twitch users
class PartialTwitchUser
{
PartialTwitchUser() = default;
QString username_;
QString id_;
public:
static PartialTwitchUser byName(const QString &username);
static PartialTwitchUser byId(const QString &id);
void getId(std::function<void(QString)> successCallback,
const QObject *caller = nullptr);
void getId(std::function<void(QString)> successCallback,
std::function<void()> failureCallback,
const QObject *caller = nullptr);
};
} // namespace chatterino
-3
View File
@@ -820,14 +820,12 @@ void PubSub::listen(rapidjson::Document &&msg)
this->addClient();
qDebug() << "Added to the back of the queue";
this->requests.emplace_back(
std::make_unique<rapidjson::Document>(std::move(msg)));
}
bool PubSub::tryListen(rapidjson::Document &msg)
{
qDebug() << "tryListen with" << this->clients.size() << "clients";
for (const auto &p : this->clients)
{
const auto &client = p.second;
@@ -1064,7 +1062,6 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
else
{
qDebug() << "Invalid whisper type:" << whisperType.c_str();
assert(false);
return;
}
}
+23 -31
View File
@@ -6,8 +6,8 @@
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "singletons/Emotes.hpp"
#include "util/RapidjsonHelpers.hpp"
@@ -151,12 +151,14 @@ void TwitchAccount::ignore(
const QString &targetName,
std::function<void(IgnoreResult, const QString &)> onFinished)
{
const auto onIdFetched = [this, targetName,
onFinished](QString targetUserId) {
this->ignoreByID(targetUserId, targetName, onFinished); //
const auto onUserFetched = [this, targetName,
onFinished](const auto &user) {
this->ignoreByID(user.id, targetName, onFinished); //
};
PartialTwitchUser::byName(targetName).getId(onIdFetched);
const auto onUserFetchFailed = [] {};
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
}
void TwitchAccount::ignoreByID(
@@ -226,12 +228,14 @@ void TwitchAccount::unignore(
const QString &targetName,
std::function<void(UnignoreResult, const QString &message)> onFinished)
{
const auto onIdFetched = [this, targetName,
onFinished](QString targetUserId) {
this->unignoreByID(targetUserId, targetName, onFinished); //
const auto onUserFetched = [this, targetName,
onFinished](const auto &user) {
this->unignoreByID(user.id, targetName, onFinished); //
};
PartialTwitchUser::byName(targetName).getId(onIdFetched);
const auto onUserFetchFailed = [] {};
getHelix()->getUserByName(targetName, onUserFetched, onUserFetchFailed);
}
void TwitchAccount::unignoreByID(
@@ -270,28 +274,18 @@ void TwitchAccount::unignoreByID(
void TwitchAccount::checkFollow(const QString targetUserID,
std::function<void(FollowResult)> onFinished)
{
QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() +
"/follows/channels/" + targetUserID);
const auto onResponse = [onFinished](bool following, const auto &record) {
if (!following)
{
onFinished(FollowResult_NotFollowing);
return;
}
NetworkRequest(url)
onFinished(FollowResult_Following);
};
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
.onError([=](NetworkResult result) {
if (result.status() == 203)
{
onFinished(FollowResult_NotFollowing);
}
else
{
onFinished(FollowResult_Failed);
}
})
.onSuccess([=](auto result) -> Outcome {
auto document = result.parseRapidJson();
onFinished(FollowResult_Following);
return Success;
})
.execute();
getHelix()->getUserFollow(this->getUserId(), targetUserID, onResponse,
[] {});
}
void TwitchAccount::followUser(const QString userID,
@@ -392,7 +386,6 @@ void TwitchAccount::autoModAllow(const QString msgID)
QString url("https://api.twitch.tv/kraken/chat/twitchbot/approve");
auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8();
qDebug() << qba;
NetworkRequest(url, NetworkRequestType::Post)
.header("Content-Type", "application/json")
@@ -412,7 +405,6 @@ void TwitchAccount::autoModDeny(const QString msgID)
QString url("https://api.twitch.tv/kraken/chat/twitchbot/deny");
auto qba = (QString("{\"msg_id\":\"") + msgID + "\"}").toUtf8();
qDebug() << qba;
NetworkRequest(url, NetworkRequestType::Post)
.header("Content-Type", "application/json")
@@ -3,11 +3,14 @@
#include "common/Common.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
namespace chatterino {
TwitchAccountManager::TwitchAccountManager()
: anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
: accounts(SharedPtrElementLess<TwitchAccount>{})
, anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
{
this->currentUserChanged.connect([this] {
auto currentUser = this->getCurrent();
@@ -140,6 +143,8 @@ void TwitchAccountManager::load()
if (user)
{
qDebug() << "Twitch user updated to" << newUsername;
getHelix()->update(user->getOAuthClient(), user->getOAuthToken());
getKraken()->update(user->getOAuthClient(), user->getOAuthToken());
this->currentUser_ = user;
}
else
@@ -221,7 +226,7 @@ TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser(
// std::lock_guard<std::mutex> lock(this->mutex);
this->accounts.insertItem(newUser);
this->accounts.insert(newUser);
return AddUserResponse::UserAdded;
}
@@ -51,9 +51,7 @@ public:
pajlada::Signals::NoArgSignal currentUserChanged;
pajlada::Signals::NoArgSignal userListUpdated;
SortedSignalVector<std::shared_ptr<TwitchAccount>,
SharedPtrElementLess<TwitchAccount>>
accounts;
SignalVector<std::shared_ptr<TwitchAccount>> accounts;
private:
enum class AddUserResponse {
-85
View File
@@ -1,85 +0,0 @@
#include "providers/twitch/TwitchApi.hpp"
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include <QString>
#include <QThread>
namespace chatterino {
void TwitchApi::findUserId(const QString user,
std::function<void(QString)> successCallback)
{
QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user);
NetworkRequest(requestUrl)
.authorizeTwitchV5(getDefaultClientID())
.timeout(30000)
.onSuccess([successCallback](auto result) mutable -> Outcome {
auto root = result.parseJson();
if (!root.value("users").isArray())
{
qDebug()
<< "API Error while getting user id, users is not an array";
successCallback("");
return Failure;
}
auto users = root.value("users").toArray();
if (users.size() != 1)
{
qDebug() << "API Error while getting user id, users array size "
"is not 1";
successCallback("");
return Failure;
}
if (!users[0].isObject())
{
qDebug() << "API Error while getting user id, first user is "
"not an object";
successCallback("");
return Failure;
}
auto firstUser = users[0].toObject();
auto id = firstUser.value("_id");
if (!id.isString())
{
qDebug() << "API Error: while getting user id, first user "
"object `_id` key is not a string";
successCallback("");
return Failure;
}
successCallback(id.toString());
return Success;
})
.execute();
}
void TwitchApi::findUserName(const QString userid,
std::function<void(QString)> successCallback)
{
QString requestUrl("https://api.twitch.tv/kraken/users/" + userid);
NetworkRequest(requestUrl)
.authorizeTwitchV5(getDefaultClientID())
.timeout(30000)
.onSuccess([successCallback](auto result) mutable -> Outcome {
auto root = result.parseJson();
auto name = root.value("name");
if (!name.isString())
{
qDebug() << "API Error: while getting user name, `name` is not "
"a string";
successCallback("");
return Failure;
}
successCallback(name.toString());
return Success;
})
.execute();
}
} // namespace chatterino
-19
View File
@@ -1,19 +0,0 @@
#pragma once
#include <QString>
#include <functional>
namespace chatterino {
class TwitchApi
{
public:
static void findUserId(const QString user,
std::function<void(QString)> callback);
static void findUserName(const QString userid,
std::function<void(QString)> callback);
private:
};
} // namespace chatterino
+66 -110
View File
@@ -14,6 +14,8 @@
#include "providers/twitch/TwitchCommon.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "providers/twitch/TwitchParseCheerEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/api/Kraken.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Toasts.hpp"
@@ -21,6 +23,7 @@
#include "util/PostToThread.hpp"
#include "widgets/Window.hpp"
#include <rapidjson/document.h>
#include <IrcConnection>
#include <QJsonArray>
#include <QJsonObject>
@@ -454,35 +457,25 @@ void TwitchChannel::refreshTitle()
}
this->titleRefreshedTime_ = QTime::currentTime();
QString url("https://api.twitch.tv/kraken/channels/" + roomID);
NetworkRequest::twitchRequest(url)
.onSuccess(
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
ChannelPtr shared = weak.lock();
if (!shared)
return Failure;
const auto onSuccess = [this,
weak = weakOf<Channel>(this)](const auto &channel) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
const auto document = result.parseRapidJson();
{
auto status = this->streamStatus_.access();
status->title = channel.status;
}
auto statusIt = document.FindMember("status");
this->liveStatusChanged.invoke();
};
if (statusIt == document.MemberEnd())
{
return Failure;
}
const auto onFailure = [] {};
{
auto status = this->streamStatus_.access();
if (!rj::getSafe(statusIt->value, status->title))
{
return Failure;
}
}
this->liveStatusChanged.invoke();
return Success;
})
.execute();
getKraken()->getChannel(roomID, onSuccess, onFailure);
}
void TwitchChannel::refreshLiveStatus()
@@ -497,106 +490,72 @@ void TwitchChannel::refreshLiveStatus()
return;
}
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
getHelix()->getStreamById(
roomID,
[this, weak = weakOf<Channel>(this)](bool live, const auto &stream) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
// auto request = makeGetStreamRequest(roomID, QThread::currentThread());
NetworkRequest::twitchRequest(url)
.onSuccess(
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
ChannelPtr shared = weak.lock();
if (!shared)
return Failure;
return this->parseLiveStatus(result.parseRapidJson());
})
.execute();
this->parseLiveStatus(live, stream);
},
[] {
// failure
});
}
Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
void TwitchChannel::parseLiveStatus(bool live, const HelixStream &stream)
{
if (!document.IsObject())
if (!live)
{
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)
this->setLive(false);
return Failure;
return;
}
if (!stream.HasMember("viewers") || !stream.HasMember("game") ||
!stream.HasMember("channel") || !stream.HasMember("created_at"))
{
qDebug()
<< "[TwitchChannel:refreshLiveStatus] Missing members in stream";
this->setLive(false);
return Failure;
}
const rapidjson::Value &streamChannel = stream["channel"];
if (!streamChannel.IsObject() || !streamChannel.HasMember("status"))
{
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing member "
"\"status\" in channel";
return Failure;
}
// Stream is live
{
auto status = this->streamStatus_.access();
status->viewerCount = stream["viewers"].GetUint();
status->game = stream["game"].GetString();
status->title = streamChannel["status"].GetString();
QDateTime since = QDateTime::fromString(
stream["created_at"].GetString(), Qt::ISODate);
status->viewerCount = stream.viewerCount;
if (status->gameId != stream.gameId)
{
status->gameId = stream.gameId;
// Resolve game ID to game name
getHelix()->getGameById(
stream.gameId,
[this, weak = weakOf<Channel>(this)](const auto &game) {
ChannelPtr shared = weak.lock();
if (!shared)
{
return;
}
{
auto status = this->streamStatus_.access();
status->game = game.name;
}
this->liveStatusChanged.invoke();
},
[] {
// failure
});
}
status->title = stream.title;
QDateTime since = QDateTime::fromString(stream.startedAt, Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
status->uptime = QString::number(diff / 3600) + "h " +
QString::number(diff % 3600 / 60) + "m";
status->rerun = false;
if (stream.HasMember("stream_type"))
{
status->streamType = stream["stream_type"].GetString();
}
else
{
status->streamType = QString();
}
if (stream.HasMember("broadcast_platform"))
{
const auto &broadcastPlatformValue = stream["broadcast_platform"];
if (broadcastPlatformValue.IsString())
{
const char *broadcastPlatform =
stream["broadcast_platform"].GetString();
if (strcmp(broadcastPlatform, "rerun") == 0)
{
status->rerun = true;
}
}
}
status->streamType = stream.type;
}
setLive(true);
this->setLive(true);
// Signal all listeners that the stream status has been updated
this->liveStatusChanged.invoke();
return Success;
}
void TwitchChannel::loadRecentMessages()
@@ -641,9 +600,6 @@ void TwitchChannel::loadRecentMessages()
void TwitchChannel::refreshPubsub()
{
// listen to moderation actions
if (!this->hasModRights())
return;
auto roomId = this->roomId();
if (roomId.isEmpty())
return;
+5 -3
View File
@@ -8,14 +8,15 @@
#include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
#include <rapidjson/document.h>
#include <IrcConnection>
#include <QColor>
#include <QRegularExpression>
#include <boost/optional.hpp>
#include <mutex>
#include <pajlada/signals/signalholder.hpp>
#include <mutex>
#include <unordered_map>
namespace chatterino {
@@ -43,6 +44,7 @@ public:
unsigned viewerCount = 0;
QString title;
QString game;
QString gameId;
QString uptime;
QString streamType;
};
@@ -120,7 +122,7 @@ protected:
private:
// Methods
void refreshLiveStatus();
Outcome parseLiveStatus(const rapidjson::Document &document);
void parseLiveStatus(bool live, const HelixStream &stream);
void refreshPubsub();
void refreshChatters();
void refreshBadges();
+4 -29
View File
@@ -7,10 +7,8 @@
#include "common/Common.hpp"
#include "common/Env.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/ChatroomChannel.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/PubsubClient.hpp"
#include "providers/twitch/TwitchAccount.hpp"
@@ -24,26 +22,13 @@ using namespace std::chrono_literals;
namespace chatterino {
namespace {
bool isChatroom(const QString &channel)
{
if (channel.left(10) == "chatrooms:")
{
auto reflist = channel.splitRef(':');
if (reflist.size() == 3)
{
return true;
}
}
return false;
}
} // namespace
TwitchIrcServer::TwitchIrcServer()
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
{
this->initializeIrc();
this->pubsub = new PubSub;
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
@@ -100,18 +85,8 @@ void TwitchIrcServer::initializeConnection(IrcConnection *connection,
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
const QString &channelName)
{
std::shared_ptr<TwitchChannel> channel;
if (isChatroom(channelName))
{
channel = std::static_pointer_cast<TwitchChannel>(
std::shared_ptr<ChatroomChannel>(new ChatroomChannel(
channelName, this->twitchBadges, this->bttv, this->ffz)));
}
else
{
channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
channelName, this->twitchBadges, this->bttv, this->ffz));
}
auto channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
channelName, this->twitchBadges, this->bttv, this->ffz));
channel->initialize();
channel->sendMessageSignal.connect(
+20 -23
View File
@@ -2,9 +2,8 @@
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "controllers/ignores/IgnoreController.hpp"
#include "controllers/pings/PingController.hpp"
#include "controllers/ignores/IgnorePhrase.hpp"
#include "messages/Message.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp"
@@ -28,7 +27,8 @@
namespace {
const QSet<QString> zeroWidthEmotes{
"SoSnowy", "IceCold", "SantaHat", "TopHat", "ReinDeer", "CandyCane",
"SoSnowy", "IceCold", "SantaHat", "TopHat",
"ReinDeer", "CandyCane", "cvMask", "cvHazmat",
};
QColor getRandomColor(const QVariant &userId)
@@ -170,12 +170,11 @@ bool TwitchMessageBuilder::isIgnored() const
auto app = getApp();
// TODO(pajlada): Do we need to check if the phrase is valid first?
for (const auto &phrase : app->ignores->phrases)
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;
}
}
@@ -205,8 +204,7 @@ bool TwitchMessageBuilder::isIgnored() const
case ShowIgnoredUsersMessages::Never:
break;
}
qDebug() << "Blocking message because it's from blocked user"
<< user.name;
return true;
}
}
@@ -238,7 +236,7 @@ void TwitchMessageBuilder::triggerHighlights()
return;
}
if (getApp()->pings->isMuted(this->channel->getName()))
if (getCSettings().isMutedChannel(this->channel->getName()))
{
// Do nothing. Pings are muted in this channel.
return;
@@ -297,6 +295,13 @@ MessagePtr TwitchMessageBuilder::build()
this->historicalMessage_ = this->tags.contains("historical");
if (this->tags.contains("msg-id") &&
this->tags["msg-id"].toString().split(';').contains(
"highlighted-message"))
{
this->message().flags.set(MessageFlag::RedeemedHighlight);
}
// timestamp
if (this->historicalMessage_)
{
@@ -764,8 +769,7 @@ void TwitchMessageBuilder::appendUsername()
void TwitchMessageBuilder::runIgnoreReplaces(
std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes)
{
auto app = getApp();
const auto &phrases = app->ignores->phrases;
auto phrases = getCSettings().ignoredMessages.readOnly();
auto removeEmotesInRange =
[](int pos, int len,
std::vector<std::tuple<int, EmotePtr, EmoteName>>
@@ -828,7 +832,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
}
};
for (const auto &phrase : phrases)
for (const auto &phrase : *phrases)
{
if (phrase.isBlock())
{
@@ -1017,7 +1021,7 @@ void TwitchMessageBuilder::parseHighlights()
QString currentUsername = currentUser->getUserName();
if (app->highlights->blacklistContains(this->ircMessage->nick()))
if (getCSettings().isBlacklistedUser(this->ircMessage->nick()))
{
// Do nothing. We ignore highlights from this user.
return;
@@ -1056,18 +1060,14 @@ void TwitchMessageBuilder::parseHighlights()
*/
}
std::vector<HighlightPhrase> userHighlights =
app->highlights->highlightedUsers.cloneVector();
// Highlight because of sender
for (const HighlightPhrase &userHighlight : userHighlights)
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();
@@ -1111,7 +1111,7 @@ void TwitchMessageBuilder::parseHighlights()
// TODO: This vector should only be rebuilt upon highlights being changed
// fourtf: should be implemented in the HighlightsController
std::vector<HighlightPhrase> activeHighlights =
app->highlights->phrases.cloneVector();
getSettings()->highlightedMessages.cloneVector();
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0)
{
@@ -1131,9 +1131,6 @@ void TwitchMessageBuilder::parseHighlights()
continue;
}
qDebug() << "Highlight because" << this->originalMessage_ << "matches"
<< highlight.getPattern();
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = highlight.getColor();
@@ -278,6 +278,7 @@ namespace {
return true;
}
} // namespace
// Look through the results of
+368
View File
@@ -0,0 +1,368 @@
#include "providers/twitch/api/Helix.hpp"
#include "common/Outcome.hpp"
namespace chatterino {
static Helix *instance = nullptr;
void Helix::fetchUsers(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixUser>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &id : userIds)
{
urlQuery.addQueryItem("id", id);
}
for (const auto &login : userLogins)
{
urlQuery.addQueryItem("login", login);
}
// TODO: set on success and on error
this->makeRequest("users", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixUser> users;
for (const auto &jsonUser : data.toArray())
{
users.emplace_back(jsonUser.toObject());
}
successCallback(users);
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getUserByName(QString userId,
ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds;
QStringList userLogins{userId};
this->fetchUsers(
userIds, userLogins,
[successCallback,
failureCallback](const std::vector<HelixUser> &users) {
if (users.empty())
{
failureCallback();
return;
}
successCallback(users[0]);
},
failureCallback);
}
void Helix::getUserById(QString userId,
ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds{userId};
QStringList userLogins;
this->fetchUsers(
userIds, userLogins,
[successCallback, failureCallback](const auto &users) {
if (users.empty())
{
failureCallback();
return;
}
successCallback(users[0]);
},
failureCallback);
}
void Helix::fetchUsersFollows(
QString fromId, QString toId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback)
{
assert(!fromId.isEmpty() || !toId.isEmpty());
QUrlQuery urlQuery;
if (!fromId.isEmpty())
{
urlQuery.addQueryItem("from_id", fromId);
}
if (!toId.isEmpty())
{
urlQuery.addQueryItem("to_id", toId);
}
// TODO: set on success and on error
this->makeRequest("users/follows", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
if (root.empty())
{
failureCallback();
return Failure;
}
successCallback(HelixUsersFollowsResponse(root));
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getUserFollowers(
QString userId, ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback)
{
this->fetchUsersFollows("", userId, successCallback, failureCallback);
}
void Helix::getUserFollow(
QString userId, QString targetId,
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
HelixFailureCallback failureCallback)
{
this->fetchUsersFollows(
userId, targetId,
[successCallback](const auto &response) {
if (response.data.empty())
{
successCallback(false, HelixUsersFollowsRecord());
return;
}
successCallback(true, response.data[0]);
},
failureCallback);
}
void Helix::fetchStreams(
QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &id : userIds)
{
urlQuery.addQueryItem("user_id", id);
}
for (const auto &login : userLogins)
{
urlQuery.addQueryItem("user_login", login);
}
// TODO: set on success and on error
this->makeRequest("streams", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixStream> streams;
for (const auto &jsonStream : data.toArray())
{
streams.emplace_back(jsonStream.toObject());
}
successCallback(streams);
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getStreamById(QString userId,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds{userId};
QStringList userLogins;
this->fetchStreams(
userIds, userLogins,
[successCallback, failureCallback](const auto &streams) {
if (streams.empty())
{
successCallback(false, HelixStream());
return;
}
successCallback(true, streams[0]);
},
failureCallback);
}
void Helix::getStreamByName(QString userName,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds;
QStringList userLogins{userName};
this->fetchStreams(
userIds, userLogins,
[successCallback, failureCallback](const auto &streams) {
if (streams.empty())
{
successCallback(false, HelixStream());
return;
}
successCallback(true, streams[0]);
},
failureCallback);
}
///
void Helix::fetchGames(QStringList gameIds, QStringList gameNames,
ResultCallback<std::vector<HelixGame>> successCallback,
HelixFailureCallback failureCallback)
{
assert((gameIds.length() + gameNames.length()) > 0);
QUrlQuery urlQuery;
for (const auto &id : gameIds)
{
urlQuery.addQueryItem("id", id);
}
for (const auto &login : gameNames)
{
urlQuery.addQueryItem("name", login);
}
// TODO: set on success and on error
this->makeRequest("games", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixGame> games;
for (const auto &jsonStream : data.toArray())
{
games.emplace_back(jsonStream.toObject());
}
successCallback(games);
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getGameById(QString gameId,
ResultCallback<HelixGame> successCallback,
HelixFailureCallback failureCallback)
{
QStringList gameIds{gameId};
QStringList gameNames;
this->fetchGames(
gameIds, gameNames,
[successCallback, failureCallback](const auto &games) {
if (games.empty())
{
failureCallback();
return;
}
successCallback(games[0]);
},
failureCallback);
}
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
{
assert(!url.startsWith("/"));
if (this->clientId.isEmpty())
{
qDebug()
<< "Helix::makeRequest called without a client ID set BabyRage";
// return boost::none;
}
if (this->oauthToken.isEmpty())
{
qDebug()
<< "Helix::makeRequest called without an oauth token set BabyRage";
// return boost::none;
}
const QString baseUrl("https://api.twitch.tv/helix/");
QUrl fullUrl(baseUrl + url);
fullUrl.setQuery(urlQuery);
return NetworkRequest(fullUrl)
.timeout(5 * 1000)
.header("Accept", "application/json")
.header("Client-ID", this->clientId)
.header("Authorization", "Bearer " + this->oauthToken);
}
void Helix::update(QString clientId, QString oauthToken)
{
this->clientId = clientId;
this->oauthToken = oauthToken;
}
void Helix::initialize()
{
assert(instance == nullptr);
instance = new Helix();
}
Helix *getHelix()
{
assert(instance != nullptr);
return instance;
}
} // namespace chatterino
+198
View File
@@ -0,0 +1,198 @@
#pragma once
#include "common/NetworkRequest.hpp"
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QUrlQuery>
#include <boost/noncopyable.hpp>
#include <boost/optional.hpp>
#include <functional>
#include <vector>
namespace chatterino {
using HelixFailureCallback = std::function<void()>;
template <typename... T>
using ResultCallback = std::function<void(T...)>;
struct HelixUser {
QString id;
QString login;
QString displayName;
QString description;
QString profileImageUrl;
int viewCount;
explicit HelixUser(QJsonObject jsonObject)
: id(jsonObject.value("id").toString())
, login(jsonObject.value("login").toString())
, displayName(jsonObject.value("display_name").toString())
, description(jsonObject.value("description").toString())
, profileImageUrl(jsonObject.value("profile_image_url").toString())
, viewCount(jsonObject.value("view_count").toInt())
{
}
};
struct HelixUsersFollowsRecord {
QString fromId;
QString fromName;
QString toId;
QString toName;
QString followedAt; // date time object
HelixUsersFollowsRecord()
: fromId("")
, fromName("")
, toId("")
, toName("")
, followedAt("")
{
}
explicit HelixUsersFollowsRecord(QJsonObject jsonObject)
: fromId(jsonObject.value("from_id").toString())
, fromName(jsonObject.value("from_name").toString())
, toId(jsonObject.value("to_id").toString())
, toName(jsonObject.value("to_name").toString())
, followedAt(jsonObject.value("followed_at").toString())
{
}
};
struct HelixUsersFollowsResponse {
int total;
std::vector<HelixUsersFollowsRecord> data;
explicit HelixUsersFollowsResponse(QJsonObject jsonObject)
: total(jsonObject.value("total").toInt())
{
const auto &jsonData = jsonObject.value("data").toArray();
std::transform(jsonData.begin(), jsonData.end(),
std::back_inserter(this->data),
[](const QJsonValue &record) {
return HelixUsersFollowsRecord(record.toObject());
});
}
};
struct HelixStream {
QString id; // stream id
QString userId;
QString userName;
QString gameId;
QString type;
QString title;
int viewerCount;
QString startedAt;
QString language;
QString thumbnailUrl;
HelixStream()
: id("")
, userId("")
, userName("")
, gameId("")
, type("")
, title("")
, viewerCount()
, startedAt("")
, language("")
, thumbnailUrl("")
{
}
explicit HelixStream(QJsonObject jsonObject)
: id(jsonObject.value("id").toString())
, userId(jsonObject.value("user_id").toString())
, userName(jsonObject.value("user_name").toString())
, gameId(jsonObject.value("game_id").toString())
, type(jsonObject.value("type").toString())
, title(jsonObject.value("title").toString())
, viewerCount(jsonObject.value("viewer_count").toInt())
, startedAt(jsonObject.value("started_at").toString())
, language(jsonObject.value("language").toString())
, thumbnailUrl(jsonObject.value("thumbnail_url").toString())
{
}
};
struct HelixGame {
QString id; // stream id
QString name;
QString boxArtUrl;
explicit HelixGame(QJsonObject jsonObject)
: id(jsonObject.value("id").toString())
, name(jsonObject.value("name").toString())
, boxArtUrl(jsonObject.value("box_art_url").toString())
{
}
};
class Helix final : boost::noncopyable
{
public:
// https://dev.twitch.tv/docs/api/reference#get-users
void fetchUsers(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixUser>> successCallback,
HelixFailureCallback failureCallback);
void getUserByName(QString userName,
ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback);
void getUserById(QString userId, ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#get-users-follows
void fetchUsersFollows(
QString fromId, QString toId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback);
void getUserFollowers(
QString userId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback);
void getUserFollow(
QString userId, QString targetId,
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#get-streams
void fetchStreams(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback,
HelixFailureCallback failureCallback);
void getStreamById(QString userId,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback);
void getStreamByName(QString userName,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback);
// https://dev.twitch.tv/docs/api/reference#get-games
void fetchGames(QStringList gameIds, QStringList gameNames,
ResultCallback<std::vector<HelixGame>> successCallback,
HelixFailureCallback failureCallback);
void getGameById(QString gameId, ResultCallback<HelixGame> successCallback,
HelixFailureCallback failureCallback);
void update(QString clientId, QString oauthToken);
static void initialize();
private:
NetworkRequest makeRequest(QString url, QUrlQuery urlQuery);
QString clientId;
QString oauthToken;
};
Helix *getHelix();
} // namespace chatterino
+104
View File
@@ -0,0 +1,104 @@
#include "providers/twitch/api/Kraken.hpp"
#include "common/Outcome.hpp"
#include "providers/twitch/TwitchCommon.hpp"
namespace chatterino {
static Kraken *instance = nullptr;
void Kraken::getChannel(QString userId,
ResultCallback<KrakenChannel> successCallback,
KrakenFailureCallback failureCallback)
{
assert(!userId.isEmpty());
this->makeRequest("channels/" + userId, {})
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
successCallback(root);
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Kraken::getUser(QString userId, ResultCallback<KrakenUser> successCallback,
KrakenFailureCallback failureCallback)
{
assert(!userId.isEmpty());
this->makeRequest("users/" + userId, {})
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
successCallback(root);
return Success;
})
.onError([failureCallback](auto result) {
// TODO: make better xd
failureCallback();
})
.execute();
}
NetworkRequest Kraken::makeRequest(QString url, QUrlQuery urlQuery)
{
assert(!url.startsWith("/"));
if (this->clientId.isEmpty())
{
qDebug()
<< "Kraken::makeRequest called without a client ID set BabyRage";
}
const QString baseUrl("https://api.twitch.tv/kraken/");
QUrl fullUrl(baseUrl + url);
fullUrl.setQuery(urlQuery);
if (!this->oauthToken.isEmpty())
{
return NetworkRequest(fullUrl)
.timeout(5 * 1000)
.header("Accept", "application/vnd.twitchtv.v5+json")
.header("Client-ID", this->clientId)
.header("Authorization", "OAuth " + this->oauthToken);
}
return NetworkRequest(fullUrl)
.timeout(5 * 1000)
.header("Accept", "application/vnd.twitchtv.v5+json")
.header("Client-ID", this->clientId);
}
void Kraken::update(QString clientId, QString oauthToken)
{
this->clientId = clientId;
this->oauthToken = oauthToken;
}
void Kraken::initialize()
{
assert(instance == nullptr);
instance = new Kraken();
getKraken()->update(getDefaultClientID(), "");
}
Kraken *getKraken()
{
assert(instance != nullptr);
return instance;
}
} // namespace chatterino
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "common/NetworkRequest.hpp"
#include <QString>
#include <QStringList>
#include <QUrlQuery>
#include <boost/noncopyable.hpp>
#include <functional>
namespace chatterino {
using KrakenFailureCallback = std::function<void()>;
template <typename... T>
using ResultCallback = std::function<void(T...)>;
struct KrakenChannel {
const QString status;
KrakenChannel(QJsonObject jsonObject)
: status(jsonObject.value("status").toString())
{
}
};
struct KrakenUser {
const QString createdAt;
KrakenUser(QJsonObject jsonObject)
: createdAt(jsonObject.value("created_at").toString())
{
}
};
class Kraken final : boost::noncopyable
{
public:
// https://dev.twitch.tv/docs/v5/reference/users#follow-channel
void getChannel(QString userId,
ResultCallback<KrakenChannel> resultCallback,
KrakenFailureCallback failureCallback);
// https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id
void getUser(QString userId, ResultCallback<KrakenUser> resultCallback,
KrakenFailureCallback failureCallback);
void update(QString clientId, QString oauthToken);
static void initialize();
private:
NetworkRequest makeRequest(QString url, QUrlQuery urlQuery);
QString clientId;
QString oauthToken;
};
Kraken *getKraken();
} // namespace chatterino
+125
View File
@@ -0,0 +1,125 @@
# Twitch API
this folder describes what sort of API requests we do, what permissions are required for the requests etc
## Kraken (V5)
We use a bunch of Kraken (V5) in Chatterino2.
### Get User
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-by-id
Migration path: **Unknown**
* We implement this in `providers/twitch/api/Kraken.cpp getUser`
Used in:
* `UserInfoPopup` to get the "created at" date of a user
### Get Channel
URL: https://dev.twitch.tv/docs/v5/reference/channels#get-channel
Migration path: **Unknown**
* We implement this in `providers/twitch/api/Kraken.cpp getChannel`
Used in:
* `TwitchChannel::refreshTitle` to check the current stream title/game of offline channels
### Follow Channel
URL: https://dev.twitch.tv/docs/v5/reference/users#follow-channel
Requires `user_follows_edit` scope
Migration path: **Unknown**
* We implement this API in `providers/twitch/TwitchAccount.cpp followUser`
### Unfollow Channel
URL: https://dev.twitch.tv/docs/v5/reference/users#unfollow-channel
Requires `user_follows_edit` scope
Migration path: **Unknown**
* We implement this API in `providers/twitch/TwitchAccount.cpp unfollowUser`
### Get Cheermotes
URL: https://dev.twitch.tv/docs/v5/reference/bits#get-cheermotes
Migration path: **Not checked**
* We implement this API in `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
### Get User Block List
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-block-list
Migration path: **Unknown**
* We use this in `providers/twitch/TwitchAccount.cpp loadIgnores`
### Block User
URL: https://dev.twitch.tv/docs/v5/reference/users#block-user
Requires `user_blocks_edit` scope
Migration path: **Unknown**
* We use this in `providers/twitch/TwitchAccount.cpp ignoreByID`
### Unblock User
URL: https://dev.twitch.tv/docs/v5/reference/users#unblock-user
Requires `user_blocks_edit` scope
Migration path: **Unknown**
* We use this in `providers/twitch/TwitchAccount.cpp unignoreByID`
### Get User Emotes
URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes
Requires `user_subscriptions` scope
Migration path: **Unknown**
* We use this in `providers/twitch/TwitchAccount.cpp loadEmotes` to figure out which emotes a user is allowed to use!
### AUTOMOD APPROVE
**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2
* We use this in `providers/twitch/TwitchAccount.cpp autoModAllow` to approve an automod deny/allow question
### AUTOMOD DENY
**Unofficial** documentation: https://discuss.dev.twitch.tv/t/allowing-others-aka-bots-to-use-twitchbot-reject/8508/2
* We use this in `providers/twitch/TwitchAccount.cpp autoModDeny` to deny an automod deny/allow question
## Helix
Full Helix API reference: https://dev.twitch.tv/docs/api/reference
### Get Users
URL: https://dev.twitch.tv/docs/api/reference#get-users
* We implement this in `providers/twitch/api/Helix.cpp fetchUsers`.
Used in:
* `UserInfoPopup` to get ID and viewcount of username we clicked
* `CommandController` to power any commands that need to get a user ID
* `Toasts` to get the profile picture of a streamer who just went live
* `TwitchAccount` ignore and unignore features to translate user name to user ID
### Get Users Follows
URL: https://dev.twitch.tv/docs/api/reference#get-users-follows
* We implement this in `providers/twitch/api/Helix.cpp fetchUsersFollows`
Used in:
* `UserInfoPopup` to get number of followers a user has
### Get Streams
URL: https://dev.twitch.tv/docs/api/reference#get-streams
* We implement this in `providers/twitch/api/Helix.cpp fetchStreams`
Used in:
* `TwitchChannel` to get live status, game, title, and viewer count of a channel
* `NotificationController` to provide notifications for channels you might not have open in Chatterino, but are still interested in getting notifications for
## TMI
The TMI api is undocumented.
### Get Chatters
**Undocumented**
* We use this in `widgets/splits/Split.cpp showViewerList`
* We use this in `providers/twitch/TwitchChannel.cpp refreshChatters`
-2
View File
@@ -2,8 +2,6 @@
#include "common/Singleton.hpp"
#define GIF_FRAME_LENGTH 33
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/emoji/Emojis.hpp"
#include "providers/ffz/FfzEmotes.hpp"
-2
View File
@@ -187,8 +187,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
return;
}
qDebug() << root;
if (action == "select")
{
QString _type = root.value("type").toString();
+101
View File
@@ -1,19 +1,120 @@
#include "singletons/Settings.hpp"
#include "Application.hpp"
#include "controllers/highlights/HighlightBlacklistUser.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "controllers/ignores/IgnorePhrase.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Resources.hpp"
#include "singletons/WindowManager.hpp"
#include "util/PersistSignalVector.hpp"
#include "util/WindowsHelper.hpp"
namespace chatterino {
ConcurrentSettings *concurrentInstance_{};
ConcurrentSettings::ConcurrentSettings()
// NOTE: these do not get deleted
: highlightedMessages(*new SignalVector<HighlightPhrase>())
, highlightedUsers(*new SignalVector<HighlightPhrase>())
, blacklistedUsers(*new SignalVector<HighlightBlacklistUser>())
, ignoredMessages(*new SignalVector<IgnorePhrase>())
, mutedChannels(*new SignalVector<QString>())
, moderationActions(*new SignalVector<ModerationAction>)
{
persist(this->highlightedMessages, "/highlighting/highlights");
persist(this->blacklistedUsers, "/highlighting/blacklist");
persist(this->highlightedUsers, "/highlighting/users");
persist(this->ignoredMessages, "/ignore/phrases");
persist(this->mutedChannels, "/pings/muted");
// tagged users?
persist(this->moderationActions, "/moderation/actions");
}
bool ConcurrentSettings::isHighlightedUser(const QString &username)
{
for (const auto &highlightedUser : this->highlightedUsers)
{
if (highlightedUser.isMatch(username))
return true;
}
return false;
}
bool ConcurrentSettings::isBlacklistedUser(const QString &username)
{
auto items = this->blacklistedUsers.readOnly();
for (const auto &blacklistedUser : *items)
{
if (blacklistedUser.isMatch(username))
return true;
}
return false;
}
bool ConcurrentSettings::isMutedChannel(const QString &channelName)
{
for (const auto &channel : this->mutedChannels)
{
if (channelName.toLower() == channel.toLower())
{
return true;
}
}
return false;
}
void ConcurrentSettings::mute(const QString &channelName)
{
mutedChannels.append(channelName);
}
void ConcurrentSettings::unmute(const QString &channelName)
{
for (std::vector<int>::size_type i = 0; i != mutedChannels.raw().size();
i++)
{
if (mutedChannels.raw()[i].toLower() == channelName.toLower())
{
mutedChannels.removeAt(i);
i--;
}
}
}
bool ConcurrentSettings::toggleMutedChannel(const QString &channelName)
{
if (this->isMutedChannel(channelName))
{
unmute(channelName);
return false;
}
else
{
mute(channelName);
return true;
}
}
ConcurrentSettings &getCSettings()
{
// `concurrentInstance_` gets assigned in Settings ctor.
assert(concurrentInstance_);
return *concurrentInstance_;
}
Settings *Settings::instance_ = nullptr;
Settings::Settings(const QString &settingsDirectory)
: ABSettings(settingsDirectory)
{
instance_ = this;
concurrentInstance_ = this;
#ifdef USEWINSDK
this->autorun = isRegisteredForStartup();

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