Merge remote-tracking branch 'main_repo/master' into git_is_pepega
This commit is contained in:
+10
-10
@@ -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!"
|
||||
|
||||
@@ -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".'
|
||||
# ),
|
||||
# },
|
||||
# }
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
+15
-17
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,6 @@ chatterino--TitleLabel {
|
||||
font-family: "Segoe UI light";
|
||||
font-size: 24px;
|
||||
color: #4FC3F7;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
chatterino--DescriptionLabel {
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
@@ -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
@@ -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{};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -64,8 +64,6 @@ public:
|
||||
struct {
|
||||
QColor regular;
|
||||
QColor alternate;
|
||||
QColor highlighted;
|
||||
QColor subscription;
|
||||
// QColor whisper;
|
||||
} backgrounds;
|
||||
|
||||
|
||||
@@ -154,11 +154,6 @@ namespace {
|
||||
toBeRemoved << info.absoluteFilePath();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &&path : toBeRemoved)
|
||||
{
|
||||
qDebug() << path << QFile(path).remove();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace detail {
|
||||
boost::optional<QPixmap> first() const;
|
||||
|
||||
private:
|
||||
void processOffset();
|
||||
QVector<Frame<QPixmap>> items_;
|
||||
int index_{0};
|
||||
int durationOffset_{0};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
enum class ColorType { SelfHighlight, Subscription, Whisper };
|
||||
enum class ColorType {
|
||||
SelfHighlight,
|
||||
Subscription,
|
||||
Whisper,
|
||||
RedeemedHighlight
|
||||
};
|
||||
|
||||
class ColorProvider
|
||||
{
|
||||
|
||||
@@ -102,7 +102,7 @@ namespace {
|
||||
modBadge = std::make_shared<Emote>(Emote{
|
||||
{""},
|
||||
modBadgeImageSet,
|
||||
Tooltip{"Twitch Channel Moderator"},
|
||||
Tooltip{"Moderator"},
|
||||
modBadge1x,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,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"
|
||||
|
||||
@@ -187,8 +187,6 @@ void NativeMessagingServer::ReceiverThread::handleMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << root;
|
||||
|
||||
if (action == "select")
|
||||
{
|
||||
QString _type = root.value("type").toString();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user