Compare commits

...

46 commits

Author SHA1 Message Date
Felanbird f0c5a781d8
Merge branch 'master' into dependabot/submodules/lib/lua/src-7923dbb 2024-01-17 20:45:54 -05:00
dependabot[bot] 70b2b9a1c3
chore(deps): bump actions/cache from 3 to 4 (#5098)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 00:54:17 +00:00
dependabot[bot] 2582e34734
chore(deps): bump ZedThree/clang-tidy-review from 0.15.1 to 0.16.0 (#5097)
Bumps [ZedThree/clang-tidy-review](https://github.com/zedthree/clang-tidy-review) from 0.15.1 to 0.16.0.
- [Release notes](https://github.com/zedthree/clang-tidy-review/releases)
- [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zedthree/clang-tidy-review/compare/v0.15.1...v0.16.0)

---
updated-dependencies:
- dependency-name: ZedThree/clang-tidy-review
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-18 00:29:23 +00:00
pajlada 11838c8e16
refactor: Move TwitchBadges to Application (#5096)
* refactor: Move TwitchBadges to Application

* refactor: Use named initializers

* refactor: Use `empty()` instead of `size() > 0`

* refactor: use emplace instead of push into the callback queue
2024-01-17 23:53:10 +01:00
pajlada 7d5967c248
Use the same input padding between light & dark themes (#5095) 2024-01-17 20:34:01 +00:00
pajlada 9eeea8f203
refactor: Fix a bunch of minor things (#5094) 2024-01-17 21:05:44 +01:00
pajlada 718696db53
refactor: Un-singletonize Paths & Updates (#5092) 2024-01-16 20:56:43 +00:00
pajlada 7f935665f9
refactor: Remove the NullablePtr class (#5091) 2024-01-15 21:30:34 +00:00
pajlada 47a14c9041
clang-tidy: Make protected & private suffix underscore optional (#5090) 2024-01-15 21:31:40 +01:00
pajlada 93e2bc18fa
refactor: move Network files from src/common/ to src/common/network/ (#5089) 2024-01-15 21:28:44 +01:00
dependabot[bot] ad69755bbb
chore(deps): bump lib/signals from ca452a8 to d067706 (#5084)
Bumps [lib/signals](https://github.com/pajlada/signals) from `ca452a8` to `d067706`.
- [Commits](ca452a811d...d06770649a)

---
updated-dependencies:
- dependency-name: lib/signals
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
2024-01-15 16:53:35 +00:00
dependabot[bot] 547ff372e1
chore(deps): bump lib/serialize from bbf0a34 to 17946d6 (#5086)
Bumps [lib/serialize](https://github.com/pajlada/serialize) from `bbf0a34` to `17946d6`.
- [Commits](bbf0a34260...17946d65a4)

---
updated-dependencies:
- dependency-name: lib/serialize
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
2024-01-15 16:16:02 +00:00
dependabot[bot] 4681fb1117
chore(deps): bump ZedThree/clang-tidy-review from 0.15.0 to 0.15.1 (#5087)
Bumps [ZedThree/clang-tidy-review](https://github.com/zedthree/clang-tidy-review) from 0.15.0 to 0.15.1.
- [Release notes](https://github.com/zedthree/clang-tidy-review/releases)
- [Changelog](https://github.com/ZedThree/clang-tidy-review/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zedthree/clang-tidy-review/compare/v0.15.0...v0.15.1)

---
updated-dependencies:
- dependency-name: ZedThree/clang-tidy-review
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
2024-01-15 15:42:46 +00:00
dependabot[bot] 451e5f0bf9
chore(deps): bump lib/settings from f92bc7b to 7a0e373 (#5085)
* chore(deps): bump lib/settings from `f92bc7b` to `7a0e373`

Bumps [lib/settings](https://github.com/pajlada/settings) from `f92bc7b` to `7a0e373`.
- [Commits](f92bc7bc49...7a0e373f34)

---
updated-dependencies:
- dependency-name: lib/settings
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-15 16:15:25 +01:00
pajlada 5b6675abb4
refactor: fix clang-tidy auto*, const&, and curly braces (#5083) 2024-01-14 17:54:52 +01:00
nerix 292f9b9734
fix: ignore save requests after closing all windows (#5081) 2024-01-14 12:37:03 +00:00
pajlada 13ff11ea75
refactor: SplitOverlay (#5082) 2024-01-14 13:09:07 +01:00
nerix c4c62f2796
fix: restore focus of last split when restoring (#5080) 2024-01-14 12:06:52 +01:00
dependabot[bot] 1554d7b6a4
chore(deps): bump ZedThree/clang-tidy-review from 0.14.0 to 0.15.0 (#5078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-12 21:18:04 +00:00
fraxx 06f950a55b
Improve Streamlink documentation (#5076)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-12 15:15:42 -05:00
nerix 5c9747e08f
fix: button hover state-change not visible in some cases (#5077) 2024-01-11 23:31:33 +01:00
nerix fa5648fd9a
refactor: NetworkPrivate (#5063)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-09 00:37:15 +01:00
nerix f42ae07408
dev: Add RecentMessages benchmark (#5071)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-07 12:15:36 +00:00
nerix 78a7ebb9f9
Improve color selection and display (#5057)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-06 20:52:29 +00:00
iProdigy 693d4f401d
feat: add badges, emotes, and filters for suspicious messages (#5060)
* feat: show chat badges on suspicious user messages

* feat: display emotes in suspicious user messages

* feat: add search filters for suspicious messages

* chore: update changelog

* refactor: resolve initial nits

* fix: finish adding new filter identifier

* Comment the new message flags

* Add a list of known issues to low trust update messages

* fix: Keep shared-pointerness of the channel

Without this change, we would have the possibility of using the
TwitchChannel after the Channel itself has gone out of scope, albeit not
realistically since we just post this to a thread and parse it - there's
no networking or big delays involved. but this shows the intent better

---------

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-06 13:22:00 +00:00
pajlada 416806bb0a
refactor: Twitch PubSub client (#5059)
* Remove unused `setAccountData` function

* Move PubSub out of TwitchIrcServer and into Application

* Add changelog entry

* fix: assert feedback

* Add PubSub::unlistenPrefix as per review suggestion

* Fix tests

* quit pubsub on exit

might conflict with exit removal, so can be reverted but this shows it's possible

* Don't manually call stop on clients, it's called when the connection is closed

* nit: rename `mainThread` to `thread`

* Join in a thread!!!!!!!!
2024-01-06 13:18:37 +01:00
nerix e48d868e8c
fix: Avoid duplicate scale in settings dialog (#5069) 2024-01-06 11:28:06 +00:00
nerix 1192393039
fix: Avoid unnecessary NotebookTab updates (#5068)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-06 11:04:04 +00:00
pajlada 99b537ffd9
Add some tests for NotebookTab (#5070)
* EmptyApplication: Add asserts to rest of getters (except for getSeventvAPI)

* Theme: make getTheme call getIApp()->getThemes() instead

this allows it to be used in tests
realistically this should be deprecated & users of it should just call
getIApp()->getThemes() directly

* Use getIApp() instead of getApp() in a few places
2024-01-06 11:42:45 +01:00
fraxx 77eb9cc1e9
Add Fraxx list of contributors (#5064) 2024-01-03 13:16:39 +00:00
fraxx 4a0ef08a00
Added missing periods at mod-related messages and some system messages (#5061)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-01-03 12:24:23 +01:00
dependabot[bot] a32b962c5d
chore(deps): bump ilammy/msvc-dev-cmd from 1.12.1 to 1.13.0 (#5062)
Bumps [ilammy/msvc-dev-cmd](https://github.com/ilammy/msvc-dev-cmd) from 1.12.1 to 1.13.0.
- [Release notes](https://github.com/ilammy/msvc-dev-cmd/releases)
- [Commits](https://github.com/ilammy/msvc-dev-cmd/compare/v1.12.1...v1.13.0)

---
updated-dependencies:
- dependency-name: ilammy/msvc-dev-cmd
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 15:01:47 +01:00
pajlada 65b1ed312c
refactor: Logging (chat logger) (#5058)
It's no longer a singleton

It's now a unique_ptr that dies together with the Application

* Add getChatLogger to EmptyApplication

* unrelated change: Access Application::instance statically

* fix logging init order

* Add changelog entry
2023-12-31 12:51:40 +00:00
iProdigy 036a5f3f21
feat: show restricted chats and suspicious treatment updates (#5056)
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
2023-12-31 10:44:55 +00:00
Mm2PL 69a54d944d
Autogenerate docs/plugin-meta.lua (#5055) 2023-12-30 10:26:19 +00:00
nerix 9a2c27d258
Allow customization of whisper colors in settings (#5053) 2023-12-29 20:52:35 +00:00
nerix 60d79ef57e
Improve docs/supplemental files for plugins (#5047)
Co-authored-by: Mm2PL <mm2pl+gh@kotmisia.pl>
2023-12-29 17:12:50 +00:00
pajlada d085ab578f
refactor: Make Args less of a singleton (#5041)
This means it's no longer a singleton, and its lifetime is bound to our application.
This felt like a good small experiment to see how its changes would look
if we did this.
As a shortcut, `getApp` that is already a mega singleton keeps a
reference to Args, this means places that are a bit more difficult to
inject into call `getApp()->getArgs()` just like other things are
accessed.
2023-12-29 15:40:31 +01:00
nerix c65ebd26bd
fix: non-native drag on Windows (#5051) 2023-12-29 15:10:56 +01:00
nerix d84779f127
fix: some buttons triggering when releasing mouse outside (#5052)
Examples of buttons fixed with this: Usercard profile picture & split header mod mode button
2023-12-29 14:20:07 +01:00
SputNikPlop 04a46f60dc
chore: add SputNikPlop to the contributors list (#5046) 2023-12-28 17:28:01 +01:00
nerix d0d240136e
fix: Add check for tall messages (#5045) 2023-12-27 16:50:04 +01:00
nerix 9612eac966
perf: Only update regions with animated elements (#5043) 2023-12-27 01:12:14 +01:00
iProdigy eb12cfa50b
feat: add sound and flash alert for automod caught messages (#5026)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2023-12-25 23:17:44 +00:00
nerix 1006bf955a
perf: skip update from GIF timer if no animated elements are shown (#5042) 2023-12-25 19:04:46 +00:00
nerix 485fc5cdb4
fix: Tooltip parenting on Windows (#5040)
Fixes #5019
2023-12-25 17:17:25 +00:00
282 changed files with 6489 additions and 3628 deletions

View file

@ -45,11 +45,9 @@ CheckOptions:
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.PrivateMemberIgnoredRegexp
value: .*
- key: readability-identifier-naming.PrivateMemberSuffix
value: _
- key: readability-identifier-naming.ProtectedMemberSuffix
value: _
value: ^.*_$
- key: readability-identifier-naming.ProtectedMemberIgnoredRegexp
value: ^.*_$
- key: readability-identifier-naming.UnionCase
value: CamelCase
- key: readability-identifier-naming.GlobalConstantCase
@ -65,5 +63,11 @@ CheckOptions:
- key: readability-identifier-naming.LocalPointerIgnoredRegexp
value: ^L$
# Benchmarks
- key: readability-identifier-naming.FunctionIgnoredRegexp
value: ^BM_[^_]+$
- key: readability-identifier-naming.ClassIgnoredRegexp
value: ^BM_[^_]+$
- key: misc-const-correctness.AnalyzeValues
value: false

View file

@ -151,7 +151,7 @@ jobs:
# WINDOWS
- name: Enable Developer Command Prompt (Windows)
if: startsWith(matrix.os, 'windows')
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables (Windows)
if: startsWith(matrix.os, 'windows')
@ -174,7 +174,7 @@ jobs:
- name: Cache conan packages (Windows)
if: startsWith(matrix.os, 'windows')
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }}
path: ~/.conan2/

View file

@ -119,7 +119,7 @@ jobs:
- name: clang-tidy review
timeout-minutes: 20
uses: ZedThree/clang-tidy-review@v0.14.0
uses: ZedThree/clang-tidy-review@v0.16.0
with:
build_dir: build-clang-tidy
config_file: ".clang-tidy"
@ -145,4 +145,4 @@ jobs:
libbenchmark-dev
- name: clang-tidy-review upload
uses: ZedThree/clang-tidy-review/upload@v0.14.0
uses: ZedThree/clang-tidy-review/upload@v0.16.0

View file

@ -43,7 +43,7 @@ jobs:
run: echo "C:\Program Files (x86)\Inno Setup 6\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Build installer
id: build-installer

View file

@ -14,6 +14,6 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: ZedThree/clang-tidy-review/post@v0.14.0
- uses: ZedThree/clang-tidy-review/post@v0.16.0
with:
lgtm_comment_body: ""

View file

@ -63,7 +63,7 @@ jobs:
version: ${{ matrix.qt-version }}
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables
if: startsWith(matrix.os, 'windows')
@ -83,7 +83,7 @@ jobs:
sccache-test-${{ matrix.os }}-${{ matrix.qt-version }}
- name: Cache conan packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }}
path: ~/.conan2/

View file

@ -1,5 +1,7 @@
# JSON resources should not be prettified...
resources/*.json
benchmarks/resources/*.json
tests/resources/*.json
# ...themes should be prettified for readability.
!resources/themes/*.json
@ -7,6 +9,7 @@ resources/*.json
lib/*/
conan-pkgs/*/
cmake/sanitizers-cmake/
tools/crash-handler
# Build folders
*build-*/

View file

@ -3,7 +3,8 @@
## Unversioned
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060)
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
@ -16,8 +17,13 @@
- Minor: Add an option to use new experimental smarter emote completion. (#4987)
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
- Minor: Add a new completion API for experimental plugins feature. (#5000)
- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047)
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
- Minor: The whisper highlight color can now be configured through the settings. (#5053)
- Minor: Added missing periods at various moderator messages and commands. (#5061)
- Minor: Improved color selection and display. (#5057)
- Minor: Improved Streamlink documentation in the settings dialog. (#5076)
- Minor: Normalized the input padding between light & dark themes. (#5095)
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
@ -52,11 +58,16 @@
- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972)
- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994)
- Bugfix: Fixed some windows appearing between screens. (#4797)
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034)
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036)
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998)
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051)
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051)
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040)
- Bugfix: Fixes to section deletion in text input fields. (#5013)
- Bugfix: Show user text input within watch streak notices. (#5029)
- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052)
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
- Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077)
- Bugfix: Fixed popup windows not persisting between restarts. (#5081)
- Bugfix: Fixed splits not retaining their focus after minimizing. (#5080)
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
- Dev: Change clang-format from v14 to v16. (#4929)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
@ -83,6 +94,8 @@
- Dev: Refactor `Emoji`'s EmojiMap into a vector. (#4980)
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
- Dev: Refactor `common/Credentials`. (#4979)
- Dev: Refactor chat logger. (#5058)
- Dev: Refactor Twitch PubSub client. (#5059)
- Dev: Changed lifetime of context menus. (#4924)
- Dev: Renamed `tools` directory to `scripts`. (#5035)
- Dev: Refactor `ChannelView`, removing a bunch of clang-tidy warnings. (#4926)
@ -92,13 +105,24 @@
- Dev: Added Tests for Windows and MacOS in CI. (#4970, #5032)
- Dev: Move `clang-tidy` checker to its own CI job. (#4996)
- Dev: Refactored the Image Uploader feature. (#4971)
- Dev: Refactored the SplitOverlay code. (#5082)
- Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096)
- Dev: Moved the Network files to their own folder. (#5089)
- Dev: Fixed deadlock and use-after-free in tests. (#4981)
- Dev: Moved all `.clang-format` files to the root directory. (#5037)
- Dev: Load less message history upon reconnects. (#5001, #5018)
- Dev: Load less message history upon reconnects. (#5001)
- Dev: Removed the `NullablePtr` class. (#5091)
- Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014)
- Dev: Fixed most compiler warnings. (#5028)
- Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747)
- Dev: Refactor Args to be less of a singleton. (#5041)
- Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045)
- Dev: Autogenerate docs/plugin-meta.lua. (#5055)
- Dev: Refactor `NetworkPrivate`. (#5063)
- Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092)
- Dev: Removed duplicate scale in settings dialog. (#5069)
- Dev: Fix `NotebookTab` emitting updates for every message. (#5068)
- Dev: Added benchmark for parsing and building recent messages. (#5071)
## 2.4.6

View file

@ -1,13 +1,16 @@
project(chatterino-benchmark)
set(benchmark_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Highlights.cpp
${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp
${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
src/main.cpp
resources/bench.qrc
src/Emojis.cpp
src/Highlights.cpp
src/FormatTime.cpp
src/Helpers.cpp
src/LimitedQueue.cpp
src/LinkParser.cpp
src/RecentMessages.cpp
# Add your new file above this line!
)
@ -27,4 +30,5 @@ set_target_properties(${PROJECT_NAME}
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin"
AUTORCC ON
)

View file

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/bench">
<file>recentmessages-nymn.json</file>
<file>seventvemotes-nymn.json</file>
</qresource>
</RCC>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -145,16 +145,13 @@ static void BM_EmojiParsing(benchmark::State &state)
BENCHMARK(BM_EmojiParsing);
template <class... Args>
static void BM_EmojiParsing2(benchmark::State &state, Args &&...args)
static void BM_EmojiParsing2(benchmark::State &state, const QString &input,
int expectedNumEmojis)
{
Emojis emojis;
emojis.load();
auto argsTuple = std::make_tuple(std::move(args)...);
auto input = std::get<0>(argsTuple);
auto expectedNumEmojis = std::get<1>(argsTuple);
for (auto _ : state)
{
auto output = emojis.parse(input);

View file

@ -4,35 +4,41 @@
using namespace chatterino;
template <class... Args>
void BM_TimeFormatting(benchmark::State &state, Args &&...args)
void BM_TimeFormattingQString(benchmark::State &state, const QString &v)
{
auto args_tuple = std::make_tuple(std::move(args)...);
for (auto _ : state)
{
formatTime(std::get<0>(args_tuple));
formatTime(v);
}
}
BENCHMARK_CAPTURE(BM_TimeFormatting, 0, 0);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs0, "0");
BENCHMARK_CAPTURE(BM_TimeFormatting, 1337, 1337);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1337, "1337");
BENCHMARK_CAPTURE(BM_TimeFormatting, 623452, 623452);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs623452, "623452");
BENCHMARK_CAPTURE(BM_TimeFormatting, 8345, 8345);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs8345, "8345");
BENCHMARK_CAPTURE(BM_TimeFormatting, 314034, 314034);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs314034, "314034");
BENCHMARK_CAPTURE(BM_TimeFormatting, 27, 27);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs27, "27");
BENCHMARK_CAPTURE(BM_TimeFormatting, 34589, 34589);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs34589, "34589");
BENCHMARK_CAPTURE(BM_TimeFormatting, 3659, 3659);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs3659, "3659");
BENCHMARK_CAPTURE(BM_TimeFormatting, 1045345, 1045345);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1045345, "1045345");
BENCHMARK_CAPTURE(BM_TimeFormatting, 86432, 86432);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs86432, "86432");
BENCHMARK_CAPTURE(BM_TimeFormatting, qsempty, "");
BENCHMARK_CAPTURE(BM_TimeFormatting, qsinvalid, "asd");
void BM_TimeFormattingInt(benchmark::State &state, int v)
{
for (auto _ : state)
{
formatTime(v);
}
}
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 0, 0);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1045345, 1045345);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1337, 1337);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 27, 27);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 314034, 314034);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 34589, 34589);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 3659, 3659);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 623452, 623452);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 8345, 8345);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 86432, 86432);
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs0, "0");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1045345, "1045345");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1337, "1337");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs27, "27");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs314034, "314034");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs34589, "34589");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs3659, "3659");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs623452, "623452");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs8345, "8345");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs86432, "86432");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsempty, "");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsinvalid, "asd");

View file

@ -0,0 +1,223 @@
#include "common/Literals.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "messages/Emote.hpp"
#include "mocks/EmptyApplication.hpp"
#include "mocks/TwitchIrcServer.hpp"
#include "mocks/UserData.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/recentmessages/Impl.hpp"
#include "providers/seventv/SeventvBadges.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
#include <benchmark/benchmark.h>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QString>
#include <optional>
using namespace chatterino;
using namespace literals;
namespace {
class MockApplication : mock::EmptyApplication
{
public:
IEmotes *getEmotes() override
{
return &this->emotes;
}
IUserDataController *getUserData() override
{
return &this->userData;
}
AccountController *getAccounts() override
{
return &this->accounts;
}
ITwitchIrcServer *getTwitch() override
{
return &this->twitch;
}
ChatterinoBadges *getChatterinoBadges() override
{
return &this->chatterinoBadges;
}
FfzBadges *getFfzBadges() override
{
return &this->ffzBadges;
}
SeventvBadges *getSeventvBadges() override
{
return &this->seventvBadges;
}
HighlightController *getHighlights() override
{
return &this->highlights;
}
AccountController accounts;
Emotes emotes;
mock::UserDataController userData;
mock::MockTwitchIrcServer twitch;
ChatterinoBadges chatterinoBadges;
FfzBadges ffzBadges;
SeventvBadges seventvBadges;
HighlightController highlights;
};
std::optional<QJsonDocument> tryReadJsonFile(const QString &path)
{
QFile file(path);
if (!file.open(QFile::ReadOnly))
{
return std::nullopt;
}
QJsonParseError e;
auto doc = QJsonDocument::fromJson(file.readAll(), &e);
if (e.error != QJsonParseError::NoError)
{
return std::nullopt;
}
return doc;
}
QJsonDocument readJsonFile(const QString &path)
{
auto opt = tryReadJsonFile(path);
if (!opt)
{
_exit(1);
}
return *opt;
}
class RecentMessages
{
public:
explicit RecentMessages(const QString &name_)
: name(name_)
, chan(this->name)
{
const auto seventvEmotes =
tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name));
const auto bttvEmotes =
tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name));
const auto ffzEmotes =
tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name));
if (seventvEmotes)
{
this->chan.setSeventvEmotes(
std::make_shared<const EmoteMap>(seventv::detail::parseEmotes(
seventvEmotes->object()["emote_set"_L1]
.toObject()["emotes"_L1]
.toArray(),
false)));
}
if (bttvEmotes)
{
this->chan.setBttvEmotes(std::make_shared<const EmoteMap>(
bttv::detail::parseChannelEmotes(bttvEmotes->object(),
this->name)));
}
if (ffzEmotes)
{
this->chan.setFfzEmotes(std::make_shared<const EmoteMap>(
ffz::detail::parseChannelEmotes(ffzEmotes->object())));
}
this->messages =
readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name));
}
~RecentMessages()
{
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
virtual void run(benchmark::State &state) = 0;
protected:
QString name;
MockApplication app;
TwitchChannel chan;
QJsonDocument messages;
};
class ParseRecentMessages : public RecentMessages
{
public:
explicit ParseRecentMessages(const QString &name_)
: RecentMessages(name_)
{
}
void run(benchmark::State &state)
{
for (auto _ : state)
{
auto parsed = recentmessages::detail::parseRecentMessages(
this->messages.object());
benchmark::DoNotOptimize(parsed);
}
}
};
class BuildRecentMessages : public RecentMessages
{
public:
explicit BuildRecentMessages(const QString &name_)
: RecentMessages(name_)
{
}
void run(benchmark::State &state)
{
auto parsed = recentmessages::detail::parseRecentMessages(
this->messages.object());
for (auto _ : state)
{
auto built = recentmessages::detail::buildRecentMessages(
parsed, &this->chan);
benchmark::DoNotOptimize(built);
}
}
};
void BM_ParseRecentMessages(benchmark::State &state, const QString &name)
{
ParseRecentMessages bench(name);
bench.run(state);
}
void BM_BuildRecentMessages(benchmark::State &state, const QString &name)
{
BuildRecentMessages bench(name);
bench.run(state);
}
} // namespace
BENCHMARK_CAPTURE(BM_ParseRecentMessages, nymn, u"nymn"_s);
BENCHMARK_CAPTURE(BM_BuildRecentMessages, nymn, u"nymn"_s);

View file

@ -1,3 +1,4 @@
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include <benchmark/benchmark.h>
@ -11,6 +12,8 @@ int main(int argc, char **argv)
{
QApplication app(argc, argv);
initResources();
::benchmark::Initialize(&argc, argv);
// Ensure settings are initialized before any benchmarks are run

View file

@ -26,7 +26,7 @@ declare module c2 {
}
enum EventType {
RegisterCompletions = "RegisterCompletions",
CompletionRequested = "CompletionRequested",
}
type CbFuncCompletionsRequested = (
@ -35,7 +35,7 @@ declare module c2 {
cursor_position: number,
is_first_word: boolean
) => CompletionList;
type CbFunc<T> = T extends EventType.RegisterCompletions
type CbFunc<T> = T extends EventType.CompletionRequested
? CbFuncCompletionsRequested
: never;

View file

@ -43,7 +43,8 @@
"type": "string",
"description": "A small description of your license.",
"examples": ["MIT", "GPL-2.0-or-later"]
}
},
"$schema": { "type": "string" }
},
"required": ["name", "description", "authors", "version", "license"]
}

58
docs/plugin-meta.lua Normal file
View file

@ -0,0 +1,58 @@
---@meta Chatterino2
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
-- This file is intended to be used with LuaLS (https://luals.github.io/).
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
---@alias LogLevel integer
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
c2.LogLevel = {}
---@alias EventType integer
---@type { CompletionRequested: EventType }
c2.EventType = {}
---@class CommandContext
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
---@field channel_name string The name of the channel the command was executed in.
---@class CompletionList
---@field values string[] The completions
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
--- Registers a new command called `name` which when executed will call `handler`.
---
---@param name string The name of the command.
---@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed.
---@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
function c2.register_command(name, handler) end
--- Registers a callback to be invoked when completions for a term are requested.
---
---@param type "CompletionRequested"
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
function c2.register_callback(type, func) end
--- Sends a message to `channel` with the specified text. Also executes commands.
---
--- **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
---
---@param channel string The name of the Twitch channel
---@param text string The text to be sent
---@return boolean ok
function c2.send_msg(channel, text) end
--- Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
---
---@param channel string
---@param text string
---@return boolean ok
function c2.system_msg(channel, text) end
--- Writes a message to the Chatterino log.
---
---@param level LogLevel The desired level.
---@param ... any Values to log. Should be convertible to a string with `tostring()`.
function c2.log(level, ...) end

View file

@ -113,6 +113,42 @@ Limitations/known issues:
rebuilding the window content caused by reloading another plugin will solve this.
- Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517).
#### `register_callback("CompletionRequested", handler)`
Registers a callback (`handler`) to process completions. The callback gets the following parameters:
- `query`: The queried word.
- `full_text_content`: The whole input.
- `cursor_position`: The position of the cursor in the input.
- `is_first_word`: Flag whether `query` is the first word in the input.
Example:
| Input | `query` | `full_text_content` | `cursor_position` | `is_first_word` |
| ---------- | ------- | ------------------- | ----------------- | --------------- |
| `foo│` | `foo` | `foo` | 3 | `true` |
| `fo│o` | `fo` | `foo` | 2 | `true` |
| `foo bar│` | `bar` | `foo bar` | 7 | `false` |
| `foo │bar` | `foo` | `foo bar` | 4 | `false` |
```lua
function string.startswith(s, other)
return string.sub(s, 1, string.len(other)) == other
end
c2.register_callback(
"CompletionRequested",
function(query, full_text_content, cursor_position, is_first_word)
if ("!join"):startswith(query) then
---@type CompletionList
return { hide_others = true, values = { "!join" } }
end
---@type CompletionList
return { hide_others = false, values = {} }
end
)
```
#### `send_msg(channel, text)`
Sends a message to `channel` with the specified text. Also executes commands.

@ -1 +1 @@
Subproject commit bbf0a34260a3e8d6e6c48be57653840ac3fa8c30
Subproject commit 17946d65a41a72b447da37df6e314cded9650c32

@ -1 +1 @@
Subproject commit f92bc7bc4940bf58b7f03cefa81a78ef09752007
Subproject commit 87ed4d950319d8da1191431f5c8c84d1fdcb92a5

@ -1 +1 @@
Subproject commit ca452a811d684db42f93d6352301406754d0c536
Subproject commit d06770649a7e83db780865d09c313a876bf0f4eb

View file

@ -1,81 +1,150 @@
#pragma once
#include "Application.hpp"
#include "common/Args.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Updates.hpp"
namespace chatterino::mock {
class EmptyApplication : public IApplication
{
public:
EmptyApplication()
: updates_(this->paths_)
{
}
virtual ~EmptyApplication() = default;
const Paths &getPaths() override
{
return this->paths_;
}
const Args &getArgs() override
{
return this->args_;
}
Theme *getThemes() override
{
assert(
false &&
"EmptyApplication::getThemes was called without being initialized");
return nullptr;
}
Fonts *getFonts() override
{
assert(
false &&
"EmptyApplication::getFonts was called without being initialized");
return nullptr;
}
IEmotes *getEmotes() override
{
assert(
false &&
"EmptyApplication::getEmotes was called without being initialized");
return nullptr;
}
AccountController *getAccounts() override
{
assert(false && "EmptyApplication::getAccounts was called without "
"being initialized");
return nullptr;
}
HotkeyController *getHotkeys() override
{
assert(false && "EmptyApplication::getHotkeys was called without being "
"initialized");
return nullptr;
}
WindowManager *getWindows() override
{
assert(false && "EmptyApplication::getWindows was called without being "
"initialized");
return nullptr;
}
Toasts *getToasts() override
{
assert(
false &&
"EmptyApplication::getToasts was called without being initialized");
return nullptr;
}
CrashHandler *getCrashHandler() override
{
assert(false && "EmptyApplication::getCrashHandler was called without "
"being initialized");
return nullptr;
}
CommandController *getCommands() override
{
assert(false && "EmptyApplication::getCommands was called without "
"being initialized");
return nullptr;
}
NotificationController *getNotifications() override
{
assert(false && "EmptyApplication::getNotifications was called without "
"being initialized");
return nullptr;
}
HighlightController *getHighlights() override
{
assert(false && "EmptyApplication::getHighlights was called without "
"being initialized");
return nullptr;
}
ITwitchIrcServer *getTwitch() override
{
assert(
false &&
"EmptyApplication::getTwitch was called without being initialized");
return nullptr;
}
PubSub *getTwitchPubSub() override
{
assert(false && "getTwitchPubSub was called without being initialized");
return nullptr;
}
TwitchBadges *getTwitchBadges() override
{
assert(false && "getTwitchBadges was called without being initialized");
return nullptr;
}
Logging *getChatLogger() override
{
assert(!"getChatLogger was called without being initialized");
return nullptr;
}
ChatterinoBadges *getChatterinoBadges() override
{
assert(false && "EmptyApplication::getChatterinoBadges was called "
"without being initialized");
return nullptr;
}
FfzBadges *getFfzBadges() override
{
assert(false && "EmptyApplication::getFfzBadges was called without "
"being initialized");
return nullptr;
}
@ -87,6 +156,8 @@ public:
IUserDataController *getUserData() override
{
assert(false && "EmptyApplication::getUserData was called without "
"being initialized");
return nullptr;
}
@ -98,11 +169,15 @@ public:
ITwitchLiveController *getTwitchLiveController() override
{
assert(false && "EmptyApplication::getTwitchLiveController was called "
"without being initialized");
return nullptr;
}
ImageUploader *getImageUploader() override
{
assert(false && "EmptyApplication::getImageUploader was called without "
"being initialized");
return nullptr;
}
@ -110,6 +185,16 @@ public:
{
return nullptr;
}
Updates &getUpdates() override
{
return this->updates_;
}
private:
Paths paths_;
Args args_;
Updates updates_;
};
} // namespace chatterino::mock

BIN
resources/avatars/fraxx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

View file

@ -66,6 +66,8 @@ olafyang | https://github.com/olafyang | | Contributor
chrrs | https://github.com/chrrs | | Contributor
4rneee | https://github.com/4rneee | | Contributor
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor
SputNikPlop | https://github.com/SputNikPlop | | Contributor
fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor
# If you are a contributor add yourself above this line

12
scripts/check-clang-tidy.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eu
clang-tidy --version
find \
src/ \
tests/src/ \
benchmarks/src/ \
mocks/include/ \
-type f \( -name "*.hpp" -o -name "*.cpp" \) -print0 | parallel -0 -j16 -I {} clang-tidy --quiet "$@" "{}"

142
scripts/make_luals_meta.py Normal file
View file

@ -0,0 +1,142 @@
"""
This script generates docs/plugin-meta.lua. It accepts no arguments
It assumes comments look like:
/**
* Thing
*
* @lua@param thing boolean
* @lua@returns boolean
* @exposed name
*/
- Do not have any useful info on '/**' and '*/' lines.
- Class members are not allowed to have non-@command lines and commands different from @lua@field
Valid commands are:
1. @exposeenum [dotted.name.in_lua.last_part]
Define a table with keys of the enum. Values behind those keys aren't
written on purpose.
This generates three lines:
- An type alias of [last_part] to integer,
- A type description that describes available values of the enum,
- A global table definition for the num
2. @lua[@command]
Writes [@command] to the file as a comment, usually this is @class, @param, @return, ...
@lua@class and @lua@field have special treatment when it comes to generation of spacing new lines
3. @exposed [c2.name]
Generates a function definition line from the last `@lua@param`s.
Non-command lines of comments are written with a space after '---'
"""
from pathlib import Path
BOILERPLATE = """
---@meta Chatterino2
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
-- This file is intended to be used with LuaLS (https://luals.github.io/).
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
"""
repo_root = Path(__file__).parent.parent
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
lua_meta = repo_root / "docs" / "plugin-meta.lua"
print("Reading from", lua_api_file.relative_to(repo_root))
print("Writing to", lua_meta.relative_to(repo_root))
with lua_api_file.open("r") as f:
lines = f.read().splitlines()
# Are we in a doc comment?
comment: bool = False
# Last `@lua@param`s seen - for @exposed generation
last_params_names: list[str] = []
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
is_class = False
# The name of the next enum in lua world
expose_next_enum_as: str | None = None
# Name of the current enum in c++ world, used to generate internal typenames for
current_enum_name: str | None = None
with lua_meta.open("w") as out:
out.write(BOILERPLATE[1:]) # skip the newline after triple quote
for line in lines:
line = line.strip()
if line.startswith("enum class "):
line = line.removeprefix("enum class ")
temp = line.split(" ", 2)
current_enum_name = temp[0]
if not expose_next_enum_as:
print(
f"Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
)
current_enum_name = None
continue
current_enum_name = expose_next_enum_as.split(".", 1)[-1]
out.write("---@alias " + current_enum_name + " integer\n")
out.write("---@type { ")
# temp[1] is '{'
if len(temp) == 2: # no values on this line
continue
line = temp[2]
if current_enum_name is not None:
for i, tok in enumerate(line.split(" ")):
if tok == "};":
break
entry = tok.removesuffix(",")
if i != 0:
out.write(", ")
out.write(entry + ": " + current_enum_name)
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
print(f"Wrote enum {expose_next_enum_as} => {current_enum_name}")
current_enum_name = None
expose_next_enum_as = None
continue
if line.startswith("/**"):
comment = True
continue
elif "*/" in line:
comment = False
if not is_class:
out.write("\n")
continue
if not comment:
continue
line = line.replace("*", "", 1).lstrip()
if line == "":
out.write("---\n")
elif line.startswith("@exposeenum "):
expose_next_enum_as = line.split(" ", 1)[1]
elif line.startswith("@exposed "):
exp = line.replace("@exposed ", "", 1)
params = ", ".join(last_params_names)
out.write(f"function {exp}({params}) end\n")
print(f"Wrote function {exp}(...)")
last_params_names = []
elif line.startswith("@lua"):
command = line.replace("@lua", "", 1)
if command.startswith("@param"):
last_params_names.append(command.split(" ", 2)[1])
elif command.startswith("@class"):
print(f"Writing {command}")
if is_class:
out.write("\n")
is_class = True
elif not command.startswith("@field"):
is_class = False
out.write("---" + command + "\n")
else:
if is_class:
is_class = False
out.write("\n")
# note the space difference from the branch above
out.write("--- " + line + "\n")

View file

@ -12,6 +12,7 @@
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/sound/ISoundController.hpp"
#include "providers/seventv/SeventvAPI.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include "singletons/ImageUploader.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/PluginController.hpp"
@ -35,6 +36,7 @@
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
@ -86,6 +88,8 @@ ISoundController *makeSoundController(Settings &settings)
}
}
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
} // namespace
namespace chatterino {
@ -104,17 +108,20 @@ IApplication::IApplication()
// It will create the instances of the major classes, and connect their signals
// to each other
Application::Application(Settings &_settings, Paths &_paths)
: themes(&this->emplace<Theme>())
Application::Application(Settings &_settings, const Paths &paths,
const Args &_args, Updates &_updates)
: paths_(paths)
, args_(_args)
, themes(&this->emplace<Theme>())
, fonts(&this->emplace<Fonts>())
, emotes(&this->emplace<Emotes>())
, accounts(&this->emplace<AccountController>())
, hotkeys(&this->emplace<HotkeyController>())
, windows(&this->emplace<WindowManager>())
, windows(&this->emplace(new WindowManager(paths)))
, toasts(&this->emplace<Toasts>())
, imageUploader(&this->emplace<ImageUploader>())
, seventvAPI(&this->emplace<SeventvAPI>())
, crashHandler(&this->emplace<CrashHandler>())
, crashHandler(&this->emplace(new CrashHandler(paths)))
, commands(&this->emplace<CommandController>())
, notifications(&this->emplace<NotificationController>())
@ -123,15 +130,18 @@ Application::Application(Settings &_settings, Paths &_paths)
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
, ffzBadges(&this->emplace<FfzBadges>())
, seventvBadges(&this->emplace<SeventvBadges>())
, userData(&this->emplace<UserDataController>())
, userData(&this->emplace(new UserDataController(paths)))
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
, twitchLiveController(&this->emplace<TwitchLiveController>())
, twitchPubSub(new PubSub(TWITCH_PUBSUB_URL))
, twitchBadges(new TwitchBadges)
, logging(new Logging(_settings))
#ifdef CHATTERINO_HAVE_PLUGINS
, plugins(&this->emplace<PluginController>())
, plugins(&this->emplace(new PluginController(paths)))
#endif
, logging(&this->emplace<Logging>())
, updates(_updates)
{
this->instance = this;
Application::instance = this;
// We can safely ignore this signal's connection since the Application will always
// be destroyed after fonts
@ -140,17 +150,25 @@ Application::Application(Settings &_settings, Paths &_paths)
});
}
void Application::initialize(Settings &settings, Paths &paths)
Application::~Application() = default;
void Application::fakeDtor()
{
this->twitchPubSub.reset();
this->twitchBadges.reset();
}
void Application::initialize(Settings &settings, const Paths &paths)
{
assert(isAppInitialized == false);
isAppInitialized = true;
// Show changelog
if (!getArgs().isFramelessEmbed &&
if (!this->args_.isFramelessEmbed &&
getSettings()->currentVersion.getValue() != "" &&
getSettings()->currentVersion.getValue() != CHATTERINO_VERSION)
{
auto box = new QMessageBox(QMessageBox::Information, "Chatterino 2",
auto *box = new QMessageBox(QMessageBox::Information, "Chatterino 2",
"Show changelog?",
QMessageBox::Yes | QMessageBox::No);
box->setAttribute(Qt::WA_DeleteOnClose);
@ -161,7 +179,7 @@ void Application::initialize(Settings &settings, Paths &paths)
}
}
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
getSettings()->currentVersion.setValue(CHATTERINO_VERSION);
@ -179,12 +197,12 @@ void Application::initialize(Settings &settings, Paths &paths)
// Show crash message.
// On Windows, the crash message was already shown.
#ifndef Q_OS_WIN
if (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
if (!this->args_.isFramelessEmbed && this->args_.crashRecovery)
{
if (auto selected =
if (auto *selected =
this->windows->getMainWindow().getNotebook().getSelectedPage())
{
if (auto container = dynamic_cast<SplitContainer *>(selected))
if (auto *container = dynamic_cast<SplitContainer *>(selected))
{
for (auto &&split : container->getSplits())
{
@ -203,7 +221,7 @@ void Application::initialize(Settings &settings, Paths &paths)
this->windows->updateWordTypeMask();
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
this->initNm(paths);
}
@ -219,14 +237,14 @@ int Application::run(QApplication &qtApp)
this->twitch->connect();
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
this->windows->getMainWindow().show();
}
getSettings()->betaUpdates.connect(
[] {
Updates::instance().checkForUpdates();
[this] {
this->updates.checkForUpdates();
},
false);
@ -305,11 +323,28 @@ ITwitchLiveController *Application::getTwitchLiveController()
return this->twitchLiveController;
}
TwitchBadges *Application::getTwitchBadges()
{
assert(this->twitchBadges);
return this->twitchBadges.get();
}
ITwitchIrcServer *Application::getTwitch()
{
return this->twitch;
}
PubSub *Application::getTwitchPubSub()
{
return this->twitchPubSub.get();
}
Logging *Application::getChatLogger()
{
return this->logging.get();
}
void Application::save()
{
for (auto &singleton : this->singletons_)
@ -318,7 +353,7 @@ void Application::save()
}
}
void Application::initNm(Paths &paths)
void Application::initNm(const Paths &paths)
{
(void)paths;
@ -334,7 +369,7 @@ void Application::initPubSub()
{
// We can safely ignore these signal connections since the twitch object will always
// be destroyed before the Application
std::ignore = this->twitch->pubsub->signals_.moderation.chatCleared.connect(
std::ignore = this->twitchPubSub->moderation.chatCleared.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
@ -343,7 +378,7 @@ void Application::initPubSub()
}
QString text =
QString("%1 cleared the chat").arg(action.source.login);
QString("%1 cleared the chat.").arg(action.source.login);
auto msg = makeSystemMessage(text);
postToThread([chan, msg] {
@ -351,7 +386,7 @@ void Application::initPubSub()
});
});
std::ignore = this->twitch->pubsub->signals_.moderation.modeChanged.connect(
std::ignore = this->twitchPubSub->moderation.modeChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
@ -360,7 +395,7 @@ void Application::initPubSub()
}
QString text =
QString("%1 turned %2 %3 mode")
QString("%1 turned %2 %3 mode.")
.arg(action.source.login)
.arg(action.state == ModeChangedAction::State::On ? "on"
: "off")
@ -377,9 +412,8 @@ void Application::initPubSub()
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.moderationStateChanged
.connect([this](const auto &action) {
std::ignore = this->twitchPubSub->moderation.moderationStateChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
@ -388,7 +422,7 @@ void Application::initPubSub()
QString text;
text = QString("%1 %2 %3")
text = QString("%1 %2 %3.")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
@ -399,7 +433,7 @@ void Application::initPubSub()
});
});
std::ignore = this->twitch->pubsub->signals_.moderation.userBanned.connect(
std::ignore = this->twitchPubSub->moderation.userBanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
@ -414,8 +448,7 @@ void Application::initPubSub()
chan->addOrReplaceTimeout(msg.release());
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.messageDeleted.connect(
std::ignore = this->twitchPubSub->moderation.messageDeleted.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
@ -439,7 +472,7 @@ void Application::initPubSub()
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
const auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
@ -455,8 +488,7 @@ void Application::initPubSub()
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.userUnbanned.connect(
std::ignore = this->twitchPubSub->moderation.userUnbanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
@ -473,7 +505,95 @@ void Application::initPubSub()
});
std::ignore =
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
this->twitchPubSub->moderation.suspiciousMessageReceived.connect(
[&](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious message with unknown "
"treatment:"
<< action.treatmentString;
return;
}
// monitored chats are received over irc; in the future, we will use pubsub instead
if (action.treatment !=
PubSubLowTrustUsersMessage::Treatment::Restricted)
{
return;
}
if (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
auto twitchChannel =
std::dynamic_pointer_cast<TwitchChannel>(chan);
if (!twitchChannel)
{
return;
}
postToThread([twitchChannel, action] {
const auto p =
TwitchMessageBuilder::makeLowTrustUserMessage(
action, twitchChannel->getName(),
twitchChannel.get());
twitchChannel->addMessage(p.first);
twitchChannel->addMessage(p.second);
});
});
std::ignore =
this->twitchPubSub->moderation.suspiciousTreatmentUpdated.connect(
[&](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious user update with unknown "
"treatment:"
<< action.treatmentString;
return;
}
if (action.updatedByUserLogin.isEmpty())
{
return;
}
if (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
auto msg =
TwitchMessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg);
});
});
std::ignore = this->twitchPubSub->moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &channelID) {
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
@ -483,8 +603,7 @@ void Application::initPubSub()
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::
AutoModCaughtMessage: {
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
@ -521,8 +640,7 @@ void Application::initPubSub()
}
// handle username style based on prefered setting
switch (
getSettings()->usernameDisplayMode.getValue())
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
@ -538,8 +656,7 @@ void Application::initPubSub()
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName =
QString("%1(%2)").arg(
senderDisplayName = QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
@ -547,12 +664,13 @@ void Application::initPubSub()
}
}
action.target = ActionUser{
msg.senderUserID, msg.senderUserLogin,
action.target =
ActionUser{msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p =
makeAutomodMessage(action, chan->getName());
TwitchMessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
@ -574,8 +692,7 @@ void Application::initPubSub()
}
});
std::ignore =
this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect(
std::ignore = this->twitchPubSub->moderation.autoModMessageBlocked.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
@ -584,19 +701,18 @@ void Application::initPubSub()
}
postToThread([chan, action] {
const auto p = makeAutomodMessage(action, chan->getName());
const auto p = TwitchMessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.automodUserMessage.connect(
std::ignore = this->twitchPubSub->moderation.automodUserMessage.connect(
[&](const auto &action) {
// This condition has been set up to execute isInStreamerMode() as the last thing
// as it could end up being expensive.
if (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
if (getSettings()->streamerModeHideModActions && isInStreamerMode())
{
return;
}
@ -615,8 +731,7 @@ void Application::initPubSub()
chan->deleteMessage(msg->id);
});
std::ignore =
this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect(
std::ignore = this->twitchPubSub->moderation.automodInfoMessage.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
@ -626,13 +741,14 @@ void Application::initPubSub()
}
postToThread([chan, action] {
const auto p = makeAutomodInfoMessage(action);
const auto p =
TwitchMessageBuilder::makeAutomodInfoMessage(action);
chan->addMessage(p);
});
});
std::ignore = this->twitch->pubsub->signals_.pointReward.redeemed.connect(
[&](auto &data) {
std::ignore =
this->twitchPubSub->pointReward.redeemed.connect([&](auto &data) {
QString channelId = data.value("channel_id").toString();
if (channelId.isEmpty())
{
@ -646,35 +762,26 @@ void Application::initPubSub()
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (auto channel = dynamic_cast<TwitchChannel *>(chan.get()))
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
});
});
this->twitch->pubsub->start();
auto RequestModerationActions = [this]() {
this->twitch->pubsub->setAccount(
getApp()->accounts->twitch.getCurrent());
// TODO(pajlada): Unlisten to all authed topics instead of only
// moderation topics this->twitch->pubsub->UnlistenAllAuthedTopics();
this->twitch->pubsub->listenToWhispers();
};
this->twitchPubSub->start();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
this->accounts->twitch.currentUserChanged.connect(
[this] {
this->twitch->pubsub->unlistenAllModerationActions();
this->twitch->pubsub->unlistenAutomod();
this->twitch->pubsub->unlistenWhispers();
this->twitchPubSub->unlistenChannelModerationActions();
this->twitchPubSub->unlistenAutomod();
this->twitchPubSub->unlistenLowTrustUsers();
this->twitchPubSub->unlistenChannelPointRewards();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
},
boost::signals2::at_front);
this->accounts->twitch.currentUserChanged.connect(RequestModerationActions);
RequestModerationActions();
}
void Application::initBttvLiveUpdates()

View file

@ -11,9 +11,11 @@
namespace chatterino {
class Args;
class TwitchIrcServer;
class ITwitchIrcServer;
class PubSub;
class Updates;
class CommandController;
class AccountController;
@ -26,6 +28,7 @@ class ISoundController;
class SoundController;
class ITwitchLiveController;
class TwitchLiveController;
class TwitchBadges;
#ifdef CHATTERINO_HAVE_PLUGINS
class PluginController;
#endif
@ -54,6 +57,8 @@ public:
static IApplication *instance;
virtual const Paths &getPaths() = 0;
virtual const Args &getArgs() = 0;
virtual Theme *getThemes() = 0;
virtual Fonts *getFonts() = 0;
virtual IEmotes *getEmotes() = 0;
@ -66,18 +71,24 @@ public:
virtual HighlightController *getHighlights() = 0;
virtual NotificationController *getNotifications() = 0;
virtual ITwitchIrcServer *getTwitch() = 0;
virtual PubSub *getTwitchPubSub() = 0;
virtual Logging *getChatLogger() = 0;
virtual ChatterinoBadges *getChatterinoBadges() = 0;
virtual FfzBadges *getFfzBadges() = 0;
virtual SeventvBadges *getSeventvBadges() = 0;
virtual IUserDataController *getUserData() = 0;
virtual ISoundController *getSound() = 0;
virtual ITwitchLiveController *getTwitchLiveController() = 0;
virtual TwitchBadges *getTwitchBadges() = 0;
virtual ImageUploader *getImageUploader() = 0;
virtual SeventvAPI *getSeventvAPI() = 0;
virtual Updates &getUpdates() = 0;
};
class Application : public IApplication
{
const Paths &paths_;
const Args &args_;
std::vector<std::unique_ptr<Singleton>> singletons_;
int argc_{};
char **argv_{};
@ -85,9 +96,22 @@ class Application : public IApplication
public:
static Application *instance;
Application(Settings &settings, Paths &paths);
Application(Settings &_settings, const Paths &paths, const Args &_args,
Updates &_updates);
~Application() override;
void initialize(Settings &settings, Paths &paths);
Application(const Application &) = delete;
Application(Application &&) = delete;
Application &operator=(const Application &) = delete;
Application &operator=(Application &&) = delete;
/**
* In the interim, before we remove _exit(0); from RunGui.cpp,
* this will destroy things we know can be destroyed
*/
void fakeDtor();
void initialize(Settings &settings, const Paths &paths);
void load();
void save();
@ -118,14 +142,23 @@ public:
private:
TwitchLiveController *const twitchLiveController{};
std::unique_ptr<PubSub> twitchPubSub;
std::unique_ptr<TwitchBadges> twitchBadges;
const std::unique_ptr<Logging> logging;
public:
#ifdef CHATTERINO_HAVE_PLUGINS
PluginController *const plugins{};
#endif
/*[[deprecated]]*/ Logging *const logging{};
const Paths &getPaths() override
{
return this->paths_;
}
const Args &getArgs() override
{
return this->args_;
}
Theme *getThemes() override
{
return this->themes;
@ -168,6 +201,8 @@ public:
return this->highlights;
}
ITwitchIrcServer *getTwitch() override;
PubSub *getTwitchPubSub() override;
Logging *getChatLogger() override;
ChatterinoBadges *getChatterinoBadges() override
{
return this->chatterinoBadges;
@ -183,6 +218,7 @@ public:
IUserDataController *getUserData() override;
ISoundController *getSound() override;
ITwitchLiveController *getTwitchLiveController() override;
TwitchBadges *getTwitchBadges() override;
ImageUploader *getImageUploader() override
{
return this->imageUploader;
@ -191,6 +227,10 @@ public:
{
return this->seventvAPI;
}
Updates &getUpdates() override
{
return this->updates;
}
pajlada::Signals::NoArgSignal streamerModeChanged;
@ -199,7 +239,7 @@ private:
void initPubSub();
void initBttvLiveUpdates();
void initSeventvEventAPI();
void initNm(Paths &paths);
void initNm(const Paths &paths);
template <typename T,
typename = std::enable_if_t<std::is_base_of<Singleton, T>::value>>
@ -219,6 +259,7 @@ private:
}
NativeMessagingServer nmServer{};
Updates &updates;
};
Application *getApp();

View file

@ -33,16 +33,6 @@ set(SOURCE_FILES
common/Literals.hpp
common/Modes.cpp
common/Modes.hpp
common/NetworkCommon.cpp
common/NetworkCommon.hpp
common/NetworkManager.cpp
common/NetworkManager.hpp
common/NetworkPrivate.cpp
common/NetworkPrivate.hpp
common/NetworkRequest.cpp
common/NetworkRequest.hpp
common/NetworkResult.cpp
common/NetworkResult.hpp
common/QLogging.cpp
common/QLogging.hpp
common/WindowDescriptors.cpp
@ -50,6 +40,19 @@ set(SOURCE_FILES
common/enums/MessageOverflow.hpp
common/network/NetworkCommon.cpp
common/network/NetworkCommon.hpp
common/network/NetworkManager.cpp
common/network/NetworkManager.hpp
common/network/NetworkPrivate.cpp
common/network/NetworkPrivate.hpp
common/network/NetworkRequest.cpp
common/network/NetworkRequest.hpp
common/network/NetworkResult.cpp
common/network/NetworkResult.hpp
common/network/NetworkTask.cpp
common/network/NetworkTask.hpp
controllers/accounts/Account.cpp
controllers/accounts/Account.hpp
controllers/accounts/AccountController.cpp
@ -412,6 +415,8 @@ set(SOURCE_FILES
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
providers/twitch/pubsubmessages/Listen.cpp
providers/twitch/pubsubmessages/Listen.hpp
providers/twitch/pubsubmessages/LowTrustUsers.cpp
providers/twitch/pubsubmessages/LowTrustUsers.hpp
providers/twitch/pubsubmessages/Message.hpp
providers/twitch/pubsubmessages/Unlisten.cpp
providers/twitch/pubsubmessages/Unlisten.hpp
@ -455,6 +460,7 @@ set(SOURCE_FILES
singletons/helper/LoggingChannel.cpp
singletons/helper/LoggingChannel.hpp
util/AbandonObject.hpp
util/AttachToConsole.cpp
util/AttachToConsole.hpp
util/CancellationToken.hpp
@ -588,12 +594,25 @@ set(SOURCE_FILES
widgets/dialogs/switcher/SwitchSplitItem.cpp
widgets/dialogs/switcher/SwitchSplitItem.hpp
widgets/helper/color/AlphaSlider.cpp
widgets/helper/color/AlphaSlider.hpp
widgets/helper/color/Checkerboard.cpp
widgets/helper/color/Checkerboard.hpp
widgets/helper/color/ColorButton.cpp
widgets/helper/color/ColorButton.hpp
widgets/helper/color/ColorInput.cpp
widgets/helper/color/ColorInput.hpp
widgets/helper/color/ColorItemDelegate.cpp
widgets/helper/color/ColorItemDelegate.hpp
widgets/helper/color/HueSlider.cpp
widgets/helper/color/HueSlider.hpp
widgets/helper/color/SBCanvas.cpp
widgets/helper/color/SBCanvas.hpp
widgets/helper/Button.cpp
widgets/helper/Button.hpp
widgets/helper/ChannelView.cpp
widgets/helper/ChannelView.hpp
widgets/helper/ColorButton.cpp
widgets/helper/ColorButton.hpp
widgets/helper/ComboBoxItemDelegate.cpp
widgets/helper/ComboBoxItemDelegate.hpp
widgets/helper/DebugPopup.cpp
@ -608,8 +627,6 @@ set(SOURCE_FILES
widgets/helper/NotebookButton.hpp
widgets/helper/NotebookTab.cpp
widgets/helper/NotebookTab.hpp
widgets/helper/QColorPicker.cpp
widgets/helper/QColorPicker.hpp
widgets/helper/RegExpItemDelegate.cpp
widgets/helper/RegExpItemDelegate.hpp
widgets/helper/TrimRegExpValidator.cpp
@ -679,6 +696,7 @@ set(SOURCE_FILES
widgets/splits/InputCompletionPopup.hpp
widgets/splits/Split.cpp
widgets/splits/Split.hpp
widgets/splits/SplitCommon.hpp
widgets/splits/SplitContainer.cpp
widgets/splits/SplitContainer.hpp
widgets/splits/SplitHeader.cpp
@ -918,6 +936,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
IRC_STATIC
IRC_NAMESPACE=Communi
)
if (USE_SYSTEM_QTKEYCHAIN)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CMAKE_BUILD

View file

@ -3,7 +3,7 @@
#include "Application.hpp"
#include "common/Args.hpp"
#include "common/Modes.hpp"
#include "common/NetworkManager.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/QLogging.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Paths.hpp"
@ -98,9 +98,9 @@ namespace {
installCustomPalette();
}
void showLastCrashDialog()
void showLastCrashDialog(const Args &args, const Paths &paths)
{
auto *dialog = new LastRunCrashDialog;
auto *dialog = new LastRunCrashDialog(args, paths);
// Use exec() over open() to block the app from being loaded
// and to be able to set the safe mode.
dialog->exec();
@ -223,16 +223,17 @@ namespace {
}
} // namespace
void runGui(QApplication &a, Paths &paths, Settings &settings)
void runGui(QApplication &a, const Paths &paths, Settings &settings,
const Args &args, Updates &updates)
{
initQt();
initResources();
initSignalHandler();
#ifdef Q_OS_WIN
if (getArgs().crashRecovery)
if (args.crashRecovery)
{
showLastCrashDialog();
showLastCrashDialog(args, paths);
}
#endif
@ -269,14 +270,14 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
});
chatterino::NetworkManager::init();
chatterino::Updates::instance().checkForUpdates();
updates.checkForUpdates();
Application app(settings, paths);
Application app(settings, paths, args, updates);
app.initialize(settings, paths);
app.run(a);
app.save();
if (!getArgs().dontSaveSettings)
if (!args.dontSaveSettings)
{
pajlada::Settings::SettingManager::gSave();
}
@ -288,6 +289,8 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
flushClipboard();
#endif
app.fakeDtor();
_exit(0);
}
} // namespace chatterino

View file

@ -3,8 +3,13 @@
class QApplication;
namespace chatterino {
class Args;
class Paths;
class Settings;
class Updates;
void runGui(QApplication &a, const Paths &paths, Settings &settings,
const Args &args, Updates &updates);
void runGui(QApplication &a, Paths &paths, Settings &settings);
} // namespace chatterino

View file

@ -1,4 +1,4 @@
#include "Args.hpp"
#include "common/Args.hpp"
#include "common/QLogging.hpp"
#include "debug/AssertInGuiThread.hpp"
@ -66,7 +66,7 @@ QStringList extractCommandLine(
namespace chatterino {
Args::Args(const QApplication &app)
Args::Args(const QApplication &app, const Paths &paths)
{
QCommandLineParser parser;
parser.setApplicationDescription("Chatterino 2 Client for Twitch Chat");
@ -132,7 +132,7 @@ Args::Args(const QApplication &app)
if (parser.isSet(channelLayout))
{
this->applyCustomChannelLayout(parser.value(channelLayout));
this->applyCustomChannelLayout(parser.value(channelLayout), paths);
}
this->verbose = parser.isSet(verboseOption);
@ -175,7 +175,7 @@ QStringList Args::currentArguments() const
return this->currentArguments_;
}
void Args::applyCustomChannelLayout(const QString &argValue)
void Args::applyCustomChannelLayout(const QString &argValue, const Paths &paths)
{
WindowLayout layout;
WindowDescriptor window;
@ -187,10 +187,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
window.type_ = WindowType::Main;
// Load main window layout from config file so we can use the same geometry
const QRect configMainLayout = [] {
const QString windowLayoutFile =
combinePath(getPaths()->settingsDirectory,
WindowManager::WINDOW_LAYOUT_FILENAME);
const QRect configMainLayout = [paths] {
const QString windowLayoutFile = combinePath(
paths.settingsDirectory, WindowManager::WINDOW_LAYOUT_FILENAME);
const WindowLayout configLayout =
WindowLayout::loadFromFile(windowLayoutFile);
@ -198,7 +197,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
for (const WindowDescriptor &window : configLayout.windows_)
{
if (window.type_ != WindowType::Main)
{
continue;
}
return window.geometry_;
}
@ -212,7 +213,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
for (const QString &channelArg : channelArgList)
{
if (channelArg.isEmpty())
{
continue;
}
// Twitch is default platform
QString platform = "t";
@ -248,18 +251,4 @@ void Args::applyCustomChannelLayout(const QString &argValue)
}
}
static Args *instance = nullptr;
void initArgs(const QApplication &app)
{
instance = new Args(app);
}
const Args &getArgs()
{
assert(instance);
return *instance;
}
} // namespace chatterino

View file

@ -8,6 +8,8 @@
namespace chatterino {
class Paths;
/// Command line arguments passed to Chatterino.
///
/// All accepted arguments:
@ -30,7 +32,8 @@ namespace chatterino {
class Args
{
public:
Args(const QApplication &app);
Args() = default;
Args(const QApplication &app, const Paths &paths);
bool printVersion{};
@ -55,12 +58,9 @@ public:
QStringList currentArguments() const;
private:
void applyCustomChannelLayout(const QString &argValue);
void applyCustomChannelLayout(const QString &argValue, const Paths &paths);
QStringList currentArguments_;
};
void initArgs(const QApplication &app);
const Args &getArgs();
} // namespace chatterino

View file

@ -82,7 +82,7 @@ LimitedQueueSnapshot<MessagePtr> Channel::getMessageSnapshot()
void Channel::addMessage(MessagePtr message,
std::optional<MessageFlags> overridingFlags)
{
auto app = getApp();
auto *app = getApp();
MessagePtr deleted;
if (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))
@ -101,7 +101,8 @@ void Channel::addMessage(MessagePtr message,
{
channelPlatform = "twitch";
}
app->logging->addMessage(this->name_, message, channelPlatform);
getIApp()->getChatLogger()->addMessage(this->name_, message,
channelPlatform);
}
if (this->messages_.pushBack(message, deleted))
@ -134,7 +135,7 @@ void Channel::disableAllMessages()
int snapshotLength = snapshot.size();
for (int i = 0; i < snapshotLength; i++)
{
auto &message = snapshot[i];
const auto &message = snapshot[i];
if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout,
MessageFlag::Whisper}))
{
@ -178,7 +179,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
existingMessageIds.reserve(snapshot.size());
// First, collect the ids of every message already present in the channel
for (auto &msg : snapshot)
for (const auto &msg : snapshot)
{
if (msg->flags.has(MessageFlag::System) || msg->id.isEmpty())
{
@ -195,7 +196,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
// being able to insert just-loaded historical messages at the end
// in the correct place.
auto lastMsg = snapshot[snapshot.size() - 1];
for (auto &msg : messages)
for (const auto &msg : messages)
{
// check if message already exists
if (existingMessageIds.count(msg->id) != 0)
@ -207,7 +208,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
anyInserted = true;
bool insertedFlag = false;
for (auto &snapshotMsg : snapshot)
for (const auto &snapshotMsg : snapshot)
{
if (snapshotMsg->flags.has(MessageFlag::System))
{
@ -315,7 +316,6 @@ bool Channel::isBroadcaster() const
bool Channel::hasModRights() const
{
// fourtf: check if staff
return this->isMod() || this->isBroadcaster();
}

View file

@ -27,12 +27,16 @@ void ChatterSet::updateOnlineChatters(
for (auto &&chatter : lowerCaseUsernames)
{
if (this->items.exists(chatter))
{
tmp.put(chatter, this->items.get(chatter));
// Less chatters than the limit => try to preserve as many as possible.
}
else if (lowerCaseUsernames.size() < chatterLimit)
{
tmp.put(chatter, chatter);
}
}
this->items = std::move(tmp);
}
@ -50,8 +54,10 @@ std::vector<QString> ChatterSet::filterByPrefix(const QString &prefix) const
for (auto &&item : this->items)
{
if (item.first.startsWith(lowerPrefix))
{
result.push_back(item.second);
}
}
return result;
}

View file

@ -1,5 +1,6 @@
#include "common/Credentials.hpp"
#include "Application.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
@ -40,7 +41,7 @@ bool useKeyring()
#ifdef NO_QTKEYCHAIN
return false;
#endif
if (getPaths()->isPortable())
if (getIApp()->getPaths().isPortable())
{
return false;
}
@ -55,7 +56,8 @@ bool useKeyring()
// Insecure storage:
QString insecurePath()
{
return combinePath(getPaths()->settingsDirectory, "credentials.json");
return combinePath(getIApp()->getPaths().settingsDirectory,
"credentials.json");
}
QJsonDocument loadInsecure()

View file

@ -56,10 +56,14 @@ public:
void set(T flag, bool value)
{
if (value)
{
this->set(flag);
}
else
{
this->unset(flag);
}
}
bool has(T flag) const
{

View file

@ -1,413 +0,0 @@
#include "common/NetworkPrivate.hpp"
#include "common/NetworkManager.hpp"
#include "common/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "singletons/Paths.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
namespace chatterino {
NetworkData::NetworkData()
: lifetimeManager_(new QObject)
{
DebugCount::increase("NetworkData");
}
NetworkData::~NetworkData()
{
this->lifetimeManager_->deleteLater();
DebugCount::decrease("NetworkData");
}
QString NetworkData::getHash()
{
static std::mutex mu;
std::lock_guard lock(mu);
if (this->hash_.isEmpty())
{
QByteArray bytes;
bytes.append(this->request_.url().toString().toUtf8());
for (const auto &header : this->request_.rawHeaderList())
{
bytes.append(header);
}
QByteArray hashBytes(
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256));
this->hash_ = hashBytes.toHex();
}
return this->hash_;
}
void writeToCache(const std::shared_ptr<NetworkData> &data,
const QByteArray &bytes)
{
if (data->cache_)
{
QtConcurrent::run([data, bytes] {
QFile cachedFile(getPaths()->cacheDirectory() + "/" +
data->getHash());
if (cachedFile.open(QIODevice::WriteOnly))
{
cachedFile.write(bytes);
}
});
}
}
void loadUncached(std::shared_ptr<NetworkData> &&data)
{
DebugCount::increase("http request started");
NetworkRequester requester;
auto *worker = new NetworkWorker;
worker->moveToThread(&NetworkManager::workerThread);
auto onUrlRequested = [data, worker]() mutable {
if (data->hasTimeout_)
{
data->timer_ = new QTimer();
data->timer_->setSingleShot(true);
data->timer_->start(data->timeoutMS_);
}
auto *reply = [&]() -> QNetworkReply * {
switch (data->requestType_)
{
case NetworkRequestType::Get:
return NetworkManager::accessManager.get(data->request_);
case NetworkRequestType::Put:
return NetworkManager::accessManager.put(data->request_,
data->payload_);
case NetworkRequestType::Delete:
return NetworkManager::accessManager.deleteResource(
data->request_);
case NetworkRequestType::Post:
if (data->multiPartPayload_)
{
assert(data->payload_.isNull());
return NetworkManager::accessManager.post(
data->request_, data->multiPartPayload_);
}
else
{
return NetworkManager::accessManager.post(
data->request_, data->payload_);
}
case NetworkRequestType::Patch:
if (data->multiPartPayload_)
{
assert(data->payload_.isNull());
return NetworkManager::accessManager.sendCustomRequest(
data->request_, "PATCH", data->multiPartPayload_);
}
else
{
return NetworkManager::accessManager.sendCustomRequest(
data->request_, "PATCH", data->payload_);
}
}
return nullptr;
}();
if (reply == nullptr)
{
qCDebug(chatterinoCommon) << "Unhandled request type";
return;
}
if (data->timer_ != nullptr && data->timer_->isActive())
{
QObject::connect(
data->timer_, &QTimer::timeout, worker, [reply, data]() {
qCDebug(chatterinoCommon) << "Aborted!";
reply->abort();
qCDebug(chatterinoHTTP)
<< QString("%1 [timed out] %2")
.arg(networkRequestTypes.at(
int(data->requestType_)),
data->request_.url().toString());
if (data->onError_)
{
postToThread([data] {
data->onError_(NetworkResult(
NetworkResult::NetworkError::TimeoutError, {},
{}));
});
}
if (data->finally_)
{
postToThread([data] {
data->finally_();
});
}
});
}
if (data->onReplyCreated_)
{
data->onReplyCreated_(reply);
}
auto handleReply = [data, reply]() mutable {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
// TODO(pajlada): A reply was received, kill the timeout timer
if (reply->error() != QNetworkReply::NetworkError::NoError)
{
if (reply->error() ==
QNetworkReply::NetworkError::OperationCanceledError)
{
// Operation cancelled, most likely timed out
qCDebug(chatterinoHTTP)
<< QString("%1 [cancelled] %2")
.arg(networkRequestTypes.at(
int(data->requestType_)),
data->request_.url().toString());
return;
}
if (data->onError_)
{
auto status = reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute);
if (data->requestType_ == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3")
.arg(networkRequestTypes.at(
int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString());
}
else
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3 %4")
.arg(networkRequestTypes.at(
int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString(),
QString(data->payload_));
}
// TODO: Should this always be run on the GUI thread?
postToThread([data, status, reply] {
data->onError_(NetworkResult(reply->error(), status,
reply->readAll()));
});
}
if (data->finally_)
{
postToThread([data] {
data->finally_();
});
}
return;
}
QByteArray bytes = reply->readAll();
writeToCache(data, bytes);
auto status =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
NetworkResult result(reply->error(), status, bytes);
DebugCount::increase("http request success");
// log("starting {}", data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([onSuccess = std::move(data->onSuccess_),
result = std::move(result)] {
onSuccess(result);
});
}
else
{
data->onSuccess_(result);
}
}
// log("finished {}", data->request_.url().toString());
reply->deleteLater();
if (data->requestType_ == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3")
.arg(networkRequestTypes.at(int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString());
}
else
{
qCDebug(chatterinoHTTP)
<< QString("%1 %3 %2 %4")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString(),
QString::number(status.toInt()),
QString(data->payload_));
}
if (data->finally_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([finally = std::move(data->finally_)] {
finally();
});
}
else
{
data->finally_();
}
}
};
if (data->timer_ != nullptr)
{
QObject::connect(reply, &QNetworkReply::finished, data->timer_,
&QObject::deleteLater);
}
QObject::connect(
reply, &QNetworkReply::finished, worker,
[data, handleReply, worker]() mutable {
if (data->executeConcurrently_ || isGuiThread())
{
handleReply();
delete worker;
}
else
{
postToThread(
[worker, cb = std::move(handleReply)]() mutable {
cb();
delete worker;
});
}
});
};
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
onUrlRequested);
emit requester.requestUrl();
}
// First tried to load cached, then uncached.
void loadCached(std::shared_ptr<NetworkData> &&data)
{
QFile cachedFile(getPaths()->cacheDirectory() + "/" + data->getHash());
if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly))
{
// File didn't exist OR File could not be opened
loadUncached(std::move(data));
return;
}
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
NetworkResult result(NetworkResult::NetworkError::NoError, QVariant(200),
bytes);
qCDebug(chatterinoHTTP)
<< QString("%1 [CACHED] 200 %2")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_ || isGuiThread())
{
// XXX: If outcome is Failure, we should invalidate the cache file
// somehow/somewhere
/*auto outcome =*/
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->onSuccess_(result);
}
else
{
postToThread([data, result]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->onSuccess_(result);
});
}
}
if (data->finally_)
{
if (data->executeConcurrently_ || isGuiThread())
{
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->finally_();
}
else
{
postToThread([data]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->finally_();
});
}
}
}
void load(std::shared_ptr<NetworkData> &&data)
{
if (data->cache_)
{
QtConcurrent::run([data = std::move(data)]() mutable {
loadCached(std::move(data));
});
}
else
{
loadUncached(std::move(data));
}
}
} // namespace chatterino

View file

@ -1,73 +0,0 @@
#pragma once
#include "common/NetworkCommon.hpp"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include <QPointer>
#include <QTimer>
#include <functional>
#include <memory>
class QNetworkReply;
namespace chatterino {
class NetworkResult;
class NetworkRequester : public QObject
{
Q_OBJECT
signals:
void requestUrl();
};
class NetworkWorker : public QObject
{
Q_OBJECT
signals:
void doneUrl();
};
struct NetworkData {
NetworkData();
~NetworkData();
QNetworkRequest request_;
bool hasCaller_{};
QPointer<QObject> caller_;
bool cache_{};
bool executeConcurrently_{};
NetworkReplyCreatedCallback onReplyCreated_;
NetworkErrorCallback onError_;
NetworkSuccessCallback onSuccess_;
NetworkFinallyCallback finally_;
NetworkRequestType requestType_ = NetworkRequestType::Get;
QByteArray payload_;
// lifetime secured by lifetimeManager_
QHttpMultiPart *multiPartPayload_{};
// Timer that tracks the timeout
// By default, there's no explicit timeout for the request
// to enable the timer, the "setTimeout" function needs to be called before
// execute is called
bool hasTimeout_{};
int timeoutMS_{};
QTimer *timer_ = nullptr;
QObject *lifetimeManager_;
QString getHash();
private:
QString hash_;
};
void load(std::shared_ptr<NetworkData> &&data);
} // namespace chatterino

View file

@ -1,73 +0,0 @@
#pragma once
#include <type_traits>
namespace chatterino {
template <typename T>
class NullablePtr
{
public:
NullablePtr()
: element_(nullptr)
{
}
NullablePtr(T *element)
: element_(element)
{
}
T *operator->() const
{
assert(this->hasElement());
return element_;
}
typename std::add_lvalue_reference<T>::type operator*() const
{
assert(this->hasElement());
return *element_;
}
T *get() const
{
assert(this->hasElement());
return this->element_;
}
bool isNull() const
{
return this->element_ == nullptr;
}
bool hasElement() const
{
return this->element_ != nullptr;
}
operator bool() const
{
return this->hasElement();
}
bool operator!() const
{
return !this->hasElement();
}
template <typename X = T,
typename = std::enable_if_t<!std::is_const<X>::value>>
operator NullablePtr<const T>() const
{
return NullablePtr<const T>(this->element_);
}
private:
T *element_;
};
} // namespace chatterino

View file

@ -308,10 +308,12 @@ public:
for (auto &&x : list)
{
if (x.row() != list.first().row())
{
return nullptr;
}
}
auto data = new QMimeData;
auto *data = new QMimeData;
data->setData("chatterino_row_id", QByteArray::number(list[0].row()));
return data;
}

View file

@ -17,7 +17,7 @@ public:
Singleton(Singleton &&) = delete;
Singleton &operator=(Singleton &&) = delete;
virtual void initialize(Settings &settings, Paths &paths)
virtual void initialize(Settings &settings, const Paths &paths)
{
(void)(settings);
(void)(paths);

View file

@ -1,4 +1,4 @@
#include "common/NetworkCommon.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QStringList>

View file

@ -13,7 +13,6 @@ class NetworkResult;
using NetworkSuccessCallback = std::function<void(NetworkResult)>;
using NetworkErrorCallback = std::function<void(NetworkResult)>;
using NetworkReplyCreatedCallback = std::function<void(QNetworkReply *)>;
using NetworkFinallyCallback = std::function<void()>;
enum class NetworkRequestType {
@ -23,13 +22,6 @@ enum class NetworkRequestType {
Delete,
Patch,
};
const static std::vector<QString> networkRequestTypes{
"GET", //
"POST", //
"PUT", //
"DELETE", //
"PATCH", //
};
// parseHeaderList takes a list of headers in string form,
// where each header pair is separated by semicolons (;) and the header name and value is divided by a colon (:)

View file

@ -1,4 +1,4 @@
#include "common/NetworkManager.hpp"
#include "common/network/NetworkManager.hpp"
#include <QNetworkAccessManager>

View file

@ -0,0 +1,205 @@
#include "common/network/NetworkPrivate.hpp"
#include "Application.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/network/NetworkTask.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "util/AbandonObject.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <magic_enum/magic_enum.hpp>
#include <QCryptographicHash>
#include <QElapsedTimer>
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
#ifdef NDEBUG
constexpr qsizetype SLOW_HTTP_THRESHOLD = 30;
#else
constexpr qsizetype SLOW_HTTP_THRESHOLD = 90;
#endif
using namespace chatterino::network::detail;
namespace {
using namespace chatterino;
void runCallback(bool concurrent, auto &&fn)
{
if (concurrent)
{
std::ignore = QtConcurrent::run(std::forward<decltype(fn)>(fn));
}
else
{
runInGuiThread(std::forward<decltype(fn)>(fn));
}
}
void loadUncached(std::shared_ptr<NetworkData> &&data)
{
DebugCount::increase("http request started");
NetworkRequester requester;
auto *worker = new NetworkTask(std::move(data));
worker->moveToThread(&NetworkManager::workerThread);
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
&NetworkTask::run);
emit requester.requestUrl();
}
void loadCached(std::shared_ptr<NetworkData> &&data)
{
QFile cachedFile(getIApp()->getPaths().cacheDirectory() + "/" +
data->getHash());
if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly))
{
loadUncached(std::move(data));
return;
}
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
qCDebug(chatterinoHTTP).noquote() << data->typeString() << "[CACHED] 200"
<< data->request.url().toString();
data->emitSuccess(
{NetworkResult::NetworkError::NoError, QVariant(200), bytes});
data->emitFinally();
}
} // namespace
namespace chatterino {
NetworkData::NetworkData()
{
DebugCount::increase("NetworkData");
}
NetworkData::~NetworkData()
{
DebugCount::decrease("NetworkData");
}
QString NetworkData::getHash()
{
if (this->hash_.isEmpty())
{
QByteArray bytes;
bytes.append(this->request.url().toString().toUtf8());
for (const auto &header : this->request.rawHeaderList())
{
bytes.append(header);
}
QByteArray hashBytes(
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256));
this->hash_ = hashBytes.toHex();
}
return this->hash_;
}
void NetworkData::emitSuccess(NetworkResult &&result)
{
if (!this->onSuccess)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->onSuccess), result = std::move(result),
url = this->request.url(), hasCaller = this->hasCaller,
caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
QElapsedTimer timer;
timer.start();
cb(result);
if (timer.elapsed() > SLOW_HTTP_THRESHOLD)
{
qCWarning(chatterinoHTTP)
<< "Slow HTTP success handler for" << url.toString()
<< timer.elapsed()
<< "ms (threshold:" << SLOW_HTTP_THRESHOLD << "ms)";
}
});
}
void NetworkData::emitError(NetworkResult &&result)
{
if (!this->onError)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->onError), result = std::move(result),
hasCaller = this->hasCaller, caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
cb(result);
});
}
void NetworkData::emitFinally()
{
if (!this->finally)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->finally), hasCaller = this->hasCaller,
caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
cb();
});
}
QLatin1String NetworkData::typeString() const
{
auto view = magic_enum::enum_name<NetworkRequestType>(this->requestType);
return QLatin1String{view.data(),
static_cast<QLatin1String::size_type>(view.size())};
}
void load(std::shared_ptr<NetworkData> &&data)
{
if (data->cache)
{
std::ignore = QtConcurrent::run([data = std::move(data)]() mutable {
loadCached(std::move(data));
});
}
else
{
loadUncached(std::move(data));
}
}
} // namespace chatterino

View file

@ -0,0 +1,71 @@
#pragma once
#include "common/Common.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include <QPointer>
#include <QTimer>
#include <memory>
#include <optional>
class QNetworkReply;
namespace chatterino {
class NetworkResult;
class NetworkRequester : public QObject
{
Q_OBJECT
signals:
void requestUrl();
};
class NetworkData
{
public:
NetworkData();
~NetworkData();
NetworkData(const NetworkData &) = delete;
NetworkData(NetworkData &&) = delete;
NetworkData &operator=(const NetworkData &) = delete;
NetworkData &operator=(NetworkData &&) = delete;
QNetworkRequest request;
bool hasCaller{};
QPointer<QObject> caller;
bool cache{};
bool executeConcurrently{};
NetworkSuccessCallback onSuccess;
NetworkErrorCallback onError;
NetworkFinallyCallback finally;
NetworkRequestType requestType = NetworkRequestType::Get;
QByteArray payload;
std::unique_ptr<QHttpMultiPart, DeleteLater> multiPartPayload;
/// By default, there's no explicit timeout for the request.
/// To set a timeout, use NetworkRequest's timeout method
std::optional<std::chrono::milliseconds> timeout{};
QString getHash();
void emitSuccess(NetworkResult &&result);
void emitError(NetworkResult &&result);
void emitFinally();
QLatin1String typeString() const;
private:
QString hash_;
};
void load(std::shared_ptr<NetworkData> &&data);
} // namespace chatterino

View file

@ -1,6 +1,6 @@
#include "common/NetworkRequest.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/NetworkPrivate.hpp"
#include "common/network/NetworkPrivate.hpp"
#include "common/QLogging.hpp"
#include "common/Version.hpp"
@ -16,8 +16,8 @@ NetworkRequest::NetworkRequest(const std::string &url,
NetworkRequestType requestType)
: data(new NetworkData)
{
this->data->request_.setUrl(QUrl(QString::fromStdString(url)));
this->data->requestType_ = requestType;
this->data->request.setUrl(QUrl(QString::fromStdString(url)));
this->data->requestType = requestType;
this->initializeDefaultValues();
}
@ -25,8 +25,8 @@ NetworkRequest::NetworkRequest(const std::string &url,
NetworkRequest::NetworkRequest(const QUrl &url, NetworkRequestType requestType)
: data(new NetworkData)
{
this->data->request_.setUrl(url);
this->data->requestType_ = requestType;
this->data->request.setUrl(url);
this->data->requestType = requestType;
this->initializeDefaultValues();
}
@ -35,7 +35,7 @@ NetworkRequest::~NetworkRequest() = default;
NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) &&
{
this->data->requestType_ = newRequestType;
this->data->requestType = newRequestType;
return std::move(*this);
}
@ -46,61 +46,55 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) &&
// Caller must be in gui thread
assert(caller->thread() == qApp->thread());
this->data->caller_ = const_cast<QObject *>(caller);
this->data->hasCaller_ = true;
this->data->caller = const_cast<QObject *>(caller);
this->data->hasCaller = true;
}
return std::move(*this);
}
NetworkRequest NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) &&
{
this->data->onReplyCreated_ = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) &&
{
this->data->onError_ = std::move(cb);
this->data->onError = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) &&
{
this->data->onSuccess_ = std::move(cb);
this->data->onSuccess = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) &&
{
this->data->finally_ = std::move(cb);
this->data->finally = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const char *value) &&
{
this->data->request_.setRawHeader(headerName, value);
this->data->request.setRawHeader(headerName, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const QByteArray &value) &&
{
this->data->request_.setRawHeader(headerName, value);
this->data->request.setRawHeader(headerName, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const QString &value) &&
{
this->data->request_.setRawHeader(headerName, value.toUtf8());
this->data->request.setRawHeader(headerName, value.toUtf8());
return std::move(*this);
}
NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header,
const QVariant &value) &&
{
this->data->request_.setHeader(header, value);
this->data->request.setHeader(header, value);
return std::move(*this);
}
@ -109,28 +103,26 @@ NetworkRequest NetworkRequest::headerList(
{
for (const auto &[headerName, headerValue] : headers)
{
this->data->request_.setRawHeader(headerName, headerValue);
this->data->request.setRawHeader(headerName, headerValue);
}
return std::move(*this);
}
NetworkRequest NetworkRequest::timeout(int ms) &&
{
this->data->hasTimeout_ = true;
this->data->timeoutMS_ = ms;
this->data->timeout = std::chrono::milliseconds(ms);
return std::move(*this);
}
NetworkRequest NetworkRequest::concurrent() &&
{
this->data->executeConcurrently_ = true;
this->data->executeConcurrently = true;
return std::move(*this);
}
NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) &&
{
payload->setParent(this->data->lifetimeManager_);
this->data->multiPartPayload_ = payload;
this->data->multiPartPayload = {payload, {}};
return std::move(*this);
}
@ -138,13 +130,13 @@ NetworkRequest NetworkRequest::followRedirects(bool on) &&
{
if (on)
{
this->data->request_.setAttribute(
this->data->request.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
}
else
{
this->data->request_.setAttribute(
this->data->request.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::ManualRedirectPolicy);
}
@ -154,13 +146,13 @@ NetworkRequest NetworkRequest::followRedirects(bool on) &&
NetworkRequest NetworkRequest::payload(const QByteArray &payload) &&
{
this->data->payload_ = payload;
this->data->payload = payload;
return std::move(*this);
}
NetworkRequest NetworkRequest::cache() &&
{
this->data->cache_ = true;
this->data->cache = true;
return std::move(*this);
}
@ -169,15 +161,14 @@ void NetworkRequest::execute()
this->executed_ = true;
// Only allow caching for GET request
if (this->data->cache_ &&
this->data->requestType_ != NetworkRequestType::Get)
if (this->data->cache && this->data->requestType != NetworkRequestType::Get)
{
qCDebug(chatterinoCommon) << "Can only cache GET requests!";
this->data->cache_ = false;
this->data->cache = false;
}
// Can not have a caller and be concurrent at the same time.
assert(!(this->data->caller_ && this->data->executeConcurrently_));
assert(!(this->data->caller && this->data->executeConcurrently));
load(std::move(this->data));
}
@ -189,7 +180,7 @@ void NetworkRequest::initializeDefaultValues()
Version::instance().commitHash())
.toUtf8();
this->data->request_.setRawHeader("User-Agent", userAgent);
this->data->request.setRawHeader("User-Agent", userAgent);
}
NetworkRequest NetworkRequest::json(const QJsonArray &root) &&

View file

@ -1,6 +1,6 @@
#pragma once
#include "common/NetworkCommon.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QHttpMultiPart>
@ -12,7 +12,7 @@ class QJsonDocument;
namespace chatterino {
struct NetworkData;
class NetworkData;
class NetworkRequest final
{
@ -43,7 +43,6 @@ public:
NetworkRequest type(NetworkRequestType newRequestType) &&;
NetworkRequest onReplyCreated(NetworkReplyCreatedCallback cb) &&;
NetworkRequest onError(NetworkErrorCallback cb) &&;
NetworkRequest onSuccess(NetworkSuccessCallback cb) &&;
NetworkRequest finally(NetworkFinallyCallback cb) &&;

View file

@ -1,4 +1,4 @@
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"

View file

@ -0,0 +1,191 @@
#include "common/network/NetworkTask.hpp"
#include "Application.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/network/NetworkPrivate.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "util/AbandonObject.hpp"
#include "util/DebugCount.hpp"
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
namespace chatterino::network::detail {
NetworkTask::NetworkTask(std::shared_ptr<NetworkData> &&data)
: data_(std::move(data))
{
}
NetworkTask::~NetworkTask()
{
if (this->reply_)
{
this->reply_->deleteLater();
}
}
void NetworkTask::run()
{
const auto &timeout = this->data_->timeout;
if (timeout.has_value())
{
this->timer_ = new QTimer(this);
this->timer_->setSingleShot(true);
this->timer_->start(timeout.value());
QObject::connect(this->timer_, &QTimer::timeout, this,
&NetworkTask::timeout);
}
this->reply_ = this->createReply();
if (!this->reply_)
{
this->deleteLater();
return;
}
QObject::connect(this->reply_, &QNetworkReply::finished, this,
&NetworkTask::finished);
}
QNetworkReply *NetworkTask::createReply()
{
const auto &data = this->data_;
const auto &request = this->data_->request;
auto &accessManager = NetworkManager::accessManager;
switch (this->data_->requestType)
{
case NetworkRequestType::Get:
return accessManager.get(request);
case NetworkRequestType::Put:
return accessManager.put(request, data->payload);
case NetworkRequestType::Delete:
return accessManager.deleteResource(data->request);
case NetworkRequestType::Post:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.post(request,
data->multiPartPayload.get());
}
else
{
return accessManager.post(request, data->payload);
}
case NetworkRequestType::Patch:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.sendCustomRequest(
request, "PATCH", data->multiPartPayload.get());
}
else
{
return NetworkManager::accessManager.sendCustomRequest(
request, "PATCH", data->payload);
}
}
return nullptr;
}
void NetworkTask::logReply()
{
auto status =
this->reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute)
.toInt();
if (this->data_->requestType == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << status
<< this->data_->request.url().toString();
}
else
{
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString()
<< this->data_->request.url().toString() << status
<< QString(this->data_->payload);
}
}
void NetworkTask::writeToCache(const QByteArray &bytes) const
{
std::ignore = QtConcurrent::run([data = this->data_, bytes] {
QFile cachedFile(getIApp()->getPaths().cacheDirectory() + "/" +
data->getHash());
if (cachedFile.open(QIODevice::WriteOnly))
{
cachedFile.write(bytes);
}
});
}
void NetworkTask::timeout()
{
AbandonObject guard(this);
// prevent abort() from calling finished()
QObject::disconnect(this->reply_, &QNetworkReply::finished, this,
&NetworkTask::finished);
this->reply_->abort();
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << "[timed out]"
<< this->data_->request.url().toString();
this->data_->emitError({NetworkResult::NetworkError::TimeoutError, {}, {}});
this->data_->emitFinally();
}
void NetworkTask::finished()
{
AbandonObject guard(this);
if (this->timer_)
{
this->timer_->stop();
}
auto *reply = this->reply_;
auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (reply->error() == QNetworkReply::OperationCanceledError)
{
// Operation cancelled, most likely timed out
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << "[cancelled]"
<< this->data_->request.url().toString();
return;
}
if (reply->error() != QNetworkReply::NoError)
{
this->logReply();
this->data_->emitError({reply->error(), status, reply->readAll()});
this->data_->emitFinally();
return;
}
QByteArray bytes = reply->readAll();
if (this->data_->cache)
{
this->writeToCache(bytes);
}
DebugCount::increase("http request success");
this->logReply();
this->data_->emitSuccess({reply->error(), status, bytes});
this->data_->emitFinally();
}
} // namespace chatterino::network::detail

View file

@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QTimer>
#include <memory>
class QNetworkReply;
namespace chatterino {
class NetworkData;
} // namespace chatterino
namespace chatterino::network::detail {
class NetworkTask : public QObject
{
Q_OBJECT
public:
NetworkTask(std::shared_ptr<NetworkData> &&data);
~NetworkTask() override;
NetworkTask(const NetworkTask &) = delete;
NetworkTask(NetworkTask &&) = delete;
NetworkTask &operator=(const NetworkTask &) = delete;
NetworkTask &operator=(NetworkTask &&) = delete;
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
public slots:
void run();
private:
QNetworkReply *createReply();
void logReply();
void writeToCache(const QByteArray &bytes) const;
std::shared_ptr<NetworkData> data_;
QNetworkReply *reply_{}; // parent: default (accessManager)
QTimer *timer_{}; // parent: this
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
private slots:
void timeout();
void finished();
};
} // namespace chatterino::network::detail

View file

@ -22,7 +22,7 @@ AccountController::AccountController()
this->twitch.accounts.itemRemoved.connect([this](const auto &args) {
if (args.caller != this)
{
auto &accs = this->twitch.accounts.raw();
const auto &accs = this->twitch.accounts.raw();
auto it = std::find(accs.begin(), accs.end(), args.item);
assert(it != accs.end());
@ -47,7 +47,7 @@ AccountController::AccountController()
});
}
void AccountController::initialize(Settings &settings, Paths &paths)
void AccountController::initialize(Settings &settings, const Paths &paths)
{
this->twitch.load();
}

View file

@ -21,7 +21,7 @@ public:
AccountModel *createModel(QObject *parent);
void initialize(Settings &settings, Paths &paths) override;
void initialize(Settings &settings, const Paths &paths) override;
TwitchAccountManager twitch;

View file

@ -261,7 +261,7 @@ const std::unordered_map<QString, VariableReplacer> COMMAND_VARS{
namespace chatterino {
void CommandController::initialize(Settings &, Paths &paths)
void CommandController::initialize(Settings &, const Paths &paths)
{
// Update commands map when the vector of commands has been updated
auto addFirstMatchToMap = [this](auto args) {

View file

@ -33,7 +33,7 @@ public:
bool dryRun);
QStringList getDefaultChatterinoCommandList();
void initialize(Settings &, Paths &paths) override;
void initialize(Settings &, const Paths &paths) override;
void save() override;
CommandModel *createModel(QObject *parent);

View file

@ -66,7 +66,7 @@ QString uptime(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uptime command only works in Twitch Channels"));
"The /uptime command only works in Twitch Channels."));
return "";
}
@ -188,14 +188,14 @@ QString clip(const CommandContext &ctx)
type != Channel::Type::Twitch && type != Channel::Type::TwitchWatching)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clip command only works in Twitch Channels"));
"The /clip command only works in Twitch Channels."));
return "";
}
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clip command only works in Twitch Channels"));
"The /clip command only works in Twitch Channels."));
return "";
}
@ -214,7 +214,7 @@ QString marker(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /marker command only works in Twitch channels"));
"The /marker command only works in Twitch channels."));
return "";
}
@ -520,7 +520,7 @@ QString unstableSetUserClientSideColor(const CommandContext &ctx)
{
ctx.channel->addMessage(
makeSystemMessage("The /unstable-set-user-color command only "
"works in Twitch channels"));
"works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString addModerator(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /mod command only works in Twitch channels"));
"The /mod command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString addVIP(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /vip command only works in Twitch channels"));
"The /vip command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -37,7 +37,7 @@ QString sendAnnouncement(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /announce command"));
"You must be logged in to use the /announce command."));
return "";
}

View file

@ -133,7 +133,7 @@ QString sendBan(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /ban command only works in Twitch channels")));
QString("The /ban command only works in Twitch channels.")));
return "";
}
@ -196,7 +196,7 @@ QString sendBanById(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /banid command only works in Twitch channels")));
QString("The /banid command only works in Twitch channels.")));
return "";
}
@ -241,7 +241,7 @@ QString sendTimeout(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /timeout command only works in Twitch channels")));
QString("The /timeout command only works in Twitch channels.")));
return "";
}
const auto *usageStr =

View file

@ -27,7 +27,7 @@ QString blockUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /block command only works in Twitch channels"));
"The /block command only works in Twitch channels."));
return "";
}
@ -101,7 +101,7 @@ QString unblockUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unblock command only works in Twitch channels"));
"The /unblock command only works in Twitch channels."));
return "";
}

View file

@ -111,7 +111,7 @@ QString emoteOnly(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /emoteonly command only works in Twitch channels"));
"The /emoteonly command only works in Twitch channels."));
return "";
}
@ -140,7 +140,7 @@ QString emoteOnlyOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /emoteonlyoff command only works in Twitch channels"));
"The /emoteonlyoff command only works in Twitch channels."));
return "";
}
@ -170,7 +170,7 @@ QString subscribers(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /subscribers command only works in Twitch channels"));
"The /subscribers command only works in Twitch channels."));
return "";
}
@ -200,7 +200,7 @@ QString subscribersOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /subscribersoff command only works in Twitch channels"));
"The /subscribersoff command only works in Twitch channels."));
return "";
}
@ -230,7 +230,7 @@ QString slow(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /slow command only works in Twitch channels"));
"The /slow command only works in Twitch channels."));
return "";
}
@ -277,7 +277,7 @@ QString slowOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /slowoff command only works in Twitch channels"));
"The /slowoff command only works in Twitch channels."));
return "";
}
@ -307,7 +307,7 @@ QString followers(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /followers command only works in Twitch channels"));
"The /followers command only works in Twitch channels."));
return "";
}
@ -355,7 +355,7 @@ QString followersOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /followersoff command only works in Twitch channels"));
"The /followersoff command only works in Twitch channels."));
return "";
}
@ -385,7 +385,7 @@ QString uniqueChat(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uniquechat command only works in Twitch channels"));
"The /uniquechat command only works in Twitch channels."));
return "";
}
@ -415,7 +415,7 @@ QString uniqueChatOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uniquechatoff command only works in Twitch channels"));
"The /uniquechatoff command only works in Twitch channels."));
return "";
}

View file

@ -70,7 +70,7 @@ QString chatters(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /chatters command only works in Twitch Channels"));
"The /chatters command only works in Twitch Channels."));
return "";
}
@ -80,7 +80,7 @@ QString chatters(const CommandContext &ctx)
getApp()->accounts->twitch.getCurrent()->getUserId(), 1,
[channel{ctx.channel}](auto result) {
channel->addMessage(
makeSystemMessage(QString("Chatter count: %1")
makeSystemMessage(QString("Chatter count: %1.")
.arg(localizeNumbers(result.total))));
},
[channel{ctx.channel}](auto error, auto message) {
@ -101,7 +101,7 @@ QString testChatters(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /test-chatters command only works in Twitch Channels"));
"The /test-chatters command only works in Twitch Channels."));
return "";
}

View file

@ -2,7 +2,7 @@
#include "Application.hpp"
#include "common/Channel.hpp"
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/Message.hpp"
@ -102,7 +102,7 @@ QString deleteAllMessages(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clear command only works in Twitch channels"));
"The /clear command only works in Twitch channels."));
return "";
}
@ -121,7 +121,7 @@ QString deleteOneMessage(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /delete command only works in Twitch channels"));
"The /delete command only works in Twitch channels."));
return "";
}

View file

@ -61,7 +61,7 @@ QString getModerators(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /mods command only works in Twitch Channels"));
"The /mods command only works in Twitch Channels."));
return "";
}

View file

@ -78,7 +78,7 @@ QString getVIPs(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /vips command only works in Twitch channels"));
"The /vips command only works in Twitch channels."));
return "";
}

View file

@ -125,7 +125,7 @@ QString startRaid(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /raid command only works in Twitch channels"));
"The /raid command only works in Twitch channels."));
return "";
}
@ -183,7 +183,7 @@ QString cancelRaid(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unraid command only works in Twitch channels"));
"The /unraid command only works in Twitch channels."));
return "";
}

View file

@ -23,7 +23,7 @@ QString removeModerator(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unmod command only works in Twitch channels"));
"The /unmod command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString removeVIP(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unvip command only works in Twitch channels"));
"The /unvip command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -20,7 +20,7 @@ QString sendReply(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /reply command only works in Twitch channels"));
"The /reply command only works in Twitch channels."));
return "";
}
@ -55,7 +55,7 @@ QString sendReply(const CommandContext &ctx)
}
ctx.channel->addMessage(
makeSystemMessage("A message from that user wasn't found"));
makeSystemMessage("A message from that user wasn't found."));
return "";
}

View file

@ -18,7 +18,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
QStringLiteral("The %1 command only works in Twitch channels")
QStringLiteral("The %1 command only works in Twitch channels.")
.arg(command)));
return {};
}
@ -29,7 +29,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
QStringLiteral("You must be logged in to use the %1 command")
QStringLiteral("You must be logged in to use the %1 command.")
.arg(command)));
return {};
}

View file

@ -15,12 +15,12 @@ QString sendShoutout(const CommandContext &ctx)
{
auto *twitchChannel = ctx.twitchChannel;
auto channel = ctx.channel;
auto words = &ctx.words;
const auto *words = &ctx.words;
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /shoutout command only works in Twitch channels"));
"The /shoutout command only works in Twitch channels."));
return "";
}
@ -28,7 +28,7 @@ QString sendShoutout(const CommandContext &ctx)
if (currentUser->isAnon())
{
channel->addMessage(
makeSystemMessage("You must be logged in to send shoutout"));
makeSystemMessage("You must be logged in to send shoutout."));
return "";
}

View file

@ -84,7 +84,7 @@ QString startCommercial(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /commercial command only works in Twitch channels"));
"The /commercial command only works in Twitch channels."));
return "";
}
@ -106,7 +106,7 @@ QString startCommercial(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /commercial command"));
"You must be logged in to use the /commercial command."));
return "";
}

View file

@ -94,7 +94,7 @@ QString unbanUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
QString("The %1 command only works in Twitch channels")
QString("The %1 command only works in Twitch channels.")
.arg(commandName)));
return "";
}

View file

@ -1,7 +1,7 @@
#include "controllers/commands/builtin/twitch/UpdateChannel.hpp"
#include "common/Channel.hpp"
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/api/Helix.hpp"

View file

@ -22,7 +22,7 @@ QString updateUserColor(const CommandContext &ctx)
if (!ctx.channel->isTwitchChannel())
{
ctx.channel->addMessage(makeSystemMessage(
"The /color command only works in Twitch channels"));
"The /color command only works in Twitch channels."));
return "";
}
auto user = getApp()->accounts->twitch.getCurrent();
@ -31,7 +31,7 @@ QString updateUserColor(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /color command"));
"You must be logged in to use the /color command."));
return "";
}

View file

@ -19,8 +19,10 @@ FilterSet::FilterSet(const QList<QUuid> &filterIds)
for (const auto &f : *filters)
{
if (filterIds.contains(f->getId()))
{
this->filters_.insert(f->getId(), f);
}
}
this->listener_ =
getSettings()->filterRecords.delayedItemsChanged.connect([this] {
@ -36,14 +38,18 @@ FilterSet::~FilterSet()
bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const
{
if (this->filters_.size() == 0)
{
return true;
}
filters::ContextMap context = filters::buildContextMap(m, channel.get());
for (const auto &f : this->filters_.values())
{
if (!f->valid() || !f->filter(context))
{
return false;
}
}
return true;
}

View file

@ -44,6 +44,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
* flags.whisper
* flags.reply
* flags.automod
* flags.restricted
* flags.monitored
*
* message.content
* message.length
@ -101,6 +103,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
{"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)},
{"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)},
{"message.content", m->messageText},
{"message.length", m->messageText.length()},

View file

@ -44,6 +44,8 @@ static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
{"flags.whisper", Type::Bool},
{"flags.reply", Type::Bool},
{"flags.automod", Type::Bool},
{"flags.restricted", Type::Bool},
{"flags.monitored", Type::Bool},
{"message.content", Type::String},
{"message.length", Type::Int},
};

View file

@ -105,7 +105,9 @@ QString Tokenizer::current() const
QString Tokenizer::preview() const
{
if (this->hasNext())
{
return this->tokens_.at(this->i_);
}
return "";
}
@ -172,51 +174,97 @@ const QStringList Tokenizer::allTokens()
TokenType Tokenizer::tokenize(const QString &text)
{
if (text == "&&")
{
return TokenType::AND;
}
else if (text == "||")
{
return TokenType::OR;
}
else if (text == "(")
{
return TokenType::LP;
}
else if (text == ")")
{
return TokenType::RP;
}
else if (text == "{")
{
return TokenType::LIST_START;
}
else if (text == "}")
{
return TokenType::LIST_END;
}
else if (text == ",")
{
return TokenType::COMMA;
}
else if (text == "+")
{
return TokenType::PLUS;
}
else if (text == "-")
{
return TokenType::MINUS;
}
else if (text == "*")
{
return TokenType::MULTIPLY;
}
else if (text == "/")
{
return TokenType::DIVIDE;
}
else if (text == "==")
{
return TokenType::EQ;
}
else if (text == "!=")
{
return TokenType::NEQ;
}
else if (text == "%")
{
return TokenType::MOD;
}
else if (text == "<")
{
return TokenType::LT;
}
else if (text == ">")
{
return TokenType::GT;
}
else if (text == "<=")
{
return TokenType::LTE;
}
else if (text == ">=")
{
return TokenType::GTE;
}
else if (text == "contains")
{
return TokenType::CONTAINS;
}
else if (text == "startswith")
{
return TokenType::STARTS_WITH;
}
else if (text == "endswith")
{
return TokenType::ENDS_WITH;
}
else if (text == "match")
{
return TokenType::MATCH;
}
else if (text == "!")
{
return TokenType::NOT;
}
else
{
if ((text.startsWith("r\"") || text.startsWith("ri\"")) &&
@ -226,15 +274,21 @@ TokenType Tokenizer::tokenize(const QString &text)
}
if (text.front() == '"' && text.back() == '"')
{
return TokenType::STRING;
}
if (validIdentifiersMap.keys().contains(text))
{
return TokenType::IDENTIFIER;
}
bool flag;
if (text.toInt(&flag); flag)
{
return TokenType::INT;
}
}
return TokenType::NONE;
}

View file

@ -32,6 +32,8 @@ static const QMap<QString, QString> validIdentifiersMap = {
{"flags.whisper", "whisper message?"},
{"flags.reply", "reply message?"},
{"flags.automod", "automod message?"},
{"flags.restricted", "restricted message?"},
{"flags.monitored", "monitored message?"},
{"message.content", "message text"},
{"message.length", "message length"}};

View file

@ -69,27 +69,39 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return 0;
case MINUS:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() - right.toInt();
}
return 0;
case MULTIPLY:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() * right.toInt();
}
return 0;
case DIVIDE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() / right.toInt();
}
return 0;
case MOD:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() % right.toInt();
}
return 0;
case OR:
if (convertVariantTypes(left, right, QMetaType::Bool))
{
return left.toBool() || right.toBool();
}
return false;
case AND:
if (convertVariantTypes(left, right, QMetaType::Bool))
{
return left.toBool() && right.toBool();
}
return false;
case EQ:
if (variantTypesMatch(left, right, QMetaType::QString))
@ -107,19 +119,27 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return !looselyCompareVariants(left, right);
case LT:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() < right.toInt();
}
return false;
case GT:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() > right.toInt();
}
return false;
case LTE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() <= right.toInt();
}
return false;
case GTE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() >= right.toInt();
}
return false;
case CONTAINS:
if (variantIs(left, QMetaType::QStringList) &&
@ -215,23 +235,31 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
// list must be two items
if (list.size() != 2)
{
return false;
}
// list must be a regular expression and an int
if (variantIsNot(list.at(0),
QMetaType::QRegularExpression) ||
variantIsNot(list.at(1), QMetaType::Int))
{
return false;
}
auto match =
list.at(0).toRegularExpression().match(matching);
// if matched, return nth capture group. Otherwise, return ""
if (match.hasMatch())
{
return match.captured(list.at(1).toInt());
}
else
{
return "";
}
}
default:
return false;
}
@ -263,9 +291,13 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
{
case PLUS:
if (left == Type::String)
{
return TypeClass{Type::String}; // String concatenation
}
else if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Int};
}
return IllTyped{this, "Can only add Ints or concatenate a String"};
case MINUS:
@ -273,13 +305,17 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
case DIVIDE:
case MOD:
if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Int};
}
return IllTyped{this, "Can only perform operation with Ints"};
case OR:
case AND:
if (left == Type::Bool && right == Type::Bool)
{
return TypeClass{Type::Bool};
}
return IllTyped{this,
"Can only perform logical operations with Bools"};
@ -292,37 +328,53 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
case LTE:
case GTE:
if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Bool};
}
return IllTyped{this, "Can only perform comparisons with Ints"};
case STARTS_WITH:
case ENDS_WITH:
if (isList(left))
{
return TypeClass{Type::Bool};
}
if (left == Type::String && right == Type::String)
{
return TypeClass{Type::Bool};
}
return IllTyped{
this,
"Can only perform starts/ends with a List or two Strings"};
case CONTAINS:
if (isList(left) || left == Type::Map)
{
return TypeClass{Type::Bool};
}
if (left == Type::String && right == Type::String)
{
return TypeClass{Type::Bool};
}
return IllTyped{
this,
"Can only perform contains with a List, a Map, or two Strings"};
case MATCH: {
if (left != Type::String)
{
return IllTyped{this,
"Left argument of match must be a String"};
}
if (right == Type::RegularExpression)
{
return TypeClass{Type::Bool};
if (right == Type::MatchingSpecifier) // group capturing
}
if (right == Type::MatchingSpecifier)
{ // group capturing
return TypeClass{Type::String};
}
return IllTyped{this, "Can only match on a RegularExpression or a "
"MatchingSpecifier"};

View file

@ -53,7 +53,7 @@ void BadgeHighlightModel::getRowFromItem(const HighlightBadge &item,
setFilePathItem(row[Column::SoundPath], item.getSoundUrl());
setColorItem(row[Column::Color], *item.getColor());
TwitchBadges::instance()->getBadgeIcon(
getIApp()->getTwitchBadges()->getBadgeIcon(
item.badgeName(), [item, row](QString /*name*/, const QIconPtr pixmap) {
row[Column::Badge]->setData(QVariant(*pixmap), Qt::DecorationRole);
});

View file

@ -123,7 +123,9 @@ struct Deserialize<chatterino::HighlightBadge> {
auto _color = QColor(encodedColor);
if (!_color.isValid())
{
_color = chatterino::HighlightBadge::FALLBACK_HIGHLIGHT_COLOR;
}
return chatterino::HighlightBadge(_name, _displayName, _showInMentions,
_hasAlert, _hasSound, _soundUrl,

View file

@ -204,6 +204,41 @@ void rebuildMessageHighlights(Settings &settings,
{
checks.emplace_back(highlightPhraseCheck(highlight));
}
if (settings.enableAutomodHighlight)
{
const auto highlightSound =
settings.enableAutomodHighlightSound.getValue();
const auto highlightAlert =
settings.enableAutomodHighlightTaskbar.getValue();
const auto highlightSoundUrlValue =
settings.automodHighlightSoundUrl.getValue();
checks.emplace_back(HighlightCheck{
[=](const auto & /*args*/, const auto & /*badges*/,
const auto & /*senderName*/, const auto & /*originalMessage*/,
const auto &flags,
const auto /*self*/) -> std::optional<HighlightResult> {
if (!flags.has(MessageFlag::AutoModOffendingMessage))
{
return std::nullopt;
}
std::optional<QUrl> highlightSoundUrl;
if (!highlightSoundUrlValue.isEmpty())
{
highlightSoundUrl = highlightSoundUrlValue;
}
return HighlightResult{
highlightAlert, // alert
highlightSound, // playSound
highlightSoundUrl, // customSoundUrl
nullptr, // color
false, // showInMentions
};
}});
}
}
void rebuildUserHighlights(Settings &settings,
@ -405,7 +440,8 @@ std::ostream &operator<<(std::ostream &os, const HighlightResult &result)
return os;
}
void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
void HighlightController::initialize(Settings &settings,
const Paths & /*paths*/)
{
this->rebuildListener_.addSetting(settings.enableSelfHighlight);
this->rebuildListener_.addSetting(settings.enableSelfHighlightSound);
@ -434,6 +470,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
this->rebuildListener_.addSetting(settings.enableAutomodHighlight);
this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound);
this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar);
this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl);
this->rebuildListener_.setCB([this, &settings] {
qCDebug(chatterinoHighlights)
<< "Rebuild checks because a setting changed";

View file

@ -86,7 +86,7 @@ struct HighlightCheck {
class HighlightController final : public Singleton
{
public:
void initialize(Settings &settings, Paths &paths) override;
void initialize(Settings &settings, const Paths &paths) override;
/**
* @brief Checks the given message parameters if it matches our internal checks, and returns a result

View file

@ -98,9 +98,8 @@ void HighlightModel::afterInit()
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
// auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
// setColorItem(whisperRow[Column::Color], *whisperColor, false);
whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
setColorItem(whisperRow[Column::Color], *whisperColor, false);
this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow);
@ -234,6 +233,30 @@ void HighlightModel::afterInit()
this->insertCustomRow(threadMessageRow,
HighlightRowIndexes::ThreadMessageRow);
// Highlight settings for automod caught messages
const std::vector<QStandardItem *> automodRow = this->createRow();
setBoolItem(automodRow[Column::Pattern],
getSettings()->enableAutomodHighlight.getValue(), true, false);
automodRow[Column::Pattern]->setData("AutoMod Caught Messages",
Qt::DisplayRole);
automodRow[Column::ShowInMentions]->setFlags({});
setBoolItem(automodRow[Column::FlashTaskbar],
getSettings()->enableAutomodHighlightTaskbar.getValue(), true,
false);
setBoolItem(automodRow[Column::PlaySound],
getSettings()->enableAutomodHighlightSound.getValue(), true,
false);
automodRow[Column::UseRegex]->setFlags({});
automodRow[Column::CaseSensitive]->setFlags({});
const auto automodSound =
QUrl(getSettings()->automodHighlightSoundUrl.getValue());
setFilePathItem(automodRow[Column::SoundPath], automodSound, false);
automodRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow);
}
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
@ -278,6 +301,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlight.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlight.setValue(
value.toBool());
}
}
}
break;
@ -336,6 +364,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlightTaskbar.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlightTaskbar.setValue(
value.toBool());
}
}
}
break;
@ -377,6 +410,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlightSound.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlightSound.setValue(
value.toBool());
}
}
}
break;
@ -412,6 +450,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->threadHighlightSoundUrl.setValue(
value.toString());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->automodHighlightSoundUrl.setValue(
value.toString());
}
}
}
break;
@ -419,48 +462,47 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
// Custom color
if (role == Qt::DecorationRole)
{
auto colorName = value.value<QColor>().name(QColor::HexArgb);
const auto setColor = [&](auto &setting, ColorType ty) {
auto color = value.value<QColor>();
setting.setValue(color.name(QColor::HexArgb));
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ty, color);
};
if (rowIndex == HighlightRowIndexes::SelfHighlightRow)
{
getSettings()->selfHighlightColor.setValue(colorName);
setColor(getSettings()->selfHighlightColor,
ColorType::SelfHighlight);
}
else if (rowIndex == HighlightRowIndexes::WhisperRow)
{
setColor(getSettings()->whisperHighlightColor,
ColorType::Whisper);
}
// else if (rowIndex == HighlightRowIndexes::WhisperRow)
// {
// getSettings()->whisperHighlightColor.setValue(colorName);
// }
else if (rowIndex == HighlightRowIndexes::SubRow)
{
getSettings()->subHighlightColor.setValue(colorName);
setColor(getSettings()->subHighlightColor,
ColorType::Subscription);
}
else if (rowIndex == HighlightRowIndexes::RedeemedRow)
{
getSettings()->redeemedHighlightColor.setValue(colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::RedeemedHighlight,
QColor(colorName));
setColor(getSettings()->redeemedHighlightColor,
ColorType::RedeemedHighlight);
}
else if (rowIndex == HighlightRowIndexes::FirstMessageRow)
{
getSettings()->firstMessageHighlightColor.setValue(
colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::FirstMessageHighlight,
QColor(colorName));
setColor(getSettings()->firstMessageHighlightColor,
ColorType::FirstMessageHighlight);
}
else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow)
{
getSettings()->elevatedMessageHighlightColor.setValue(
colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::ElevatedMessageHighlight,
QColor(colorName));
setColor(getSettings()->elevatedMessageHighlightColor,
ColorType::ElevatedMessageHighlight);
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->threadHighlightColor.setValue(colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::ThreadMessageHighlight,
QColor(colorName));
setColor(getSettings()->threadHighlightColor,
ColorType::ThreadMessageHighlight);
}
}
}

View file

@ -34,6 +34,7 @@ public:
FirstMessageRow = 4,
ElevatedMessageRow = 5,
ThreadMessageRow = 6,
AutomodRow = 7,
};
enum UserHighlightRowIndexes {

View file

@ -164,7 +164,9 @@ struct Deserialize<chatterino::HighlightPhrase> {
auto _color = QColor(encodedColor);
if (!_color.isValid())
{
_color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR;
}
return chatterino::HighlightPhrase(_pattern, _showInMentions, _hasAlert,
_hasSound, _isRegex,

View file

@ -1,7 +1,6 @@
#include "UserHighlightModel.hpp"
#include "controllers/highlights/UserHighlightModel.hpp"
#include "Application.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "providers/colors/ColorProvider.hpp"
#include "singletons/Settings.hpp"
@ -10,8 +9,6 @@
namespace chatterino {
using Column = HighlightModel::Column;
// commandmodel
UserHighlightModel::UserHighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(Column::COUNT, parent)

View file

@ -1,6 +1,7 @@
#pragma once
#include "common/SignalVectorModel.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include <QObject>
@ -12,6 +13,8 @@ class HighlightPhrase;
class UserHighlightModel : public SignalVectorModel<HighlightPhrase>
{
public:
using Column = HighlightModel::Column;
explicit UserHighlightModel(QObject *parent);
protected:

View file

@ -66,7 +66,7 @@ std::vector<QShortcut *> HotkeyController::shortcutsForCategory(
continue;
}
auto createShortcutFromKeySeq = [&](QKeySequence qs) {
auto s = new QShortcut(qs, parent);
auto *s = new QShortcut(qs, parent);
s->setContext(hotkey->getContext());
auto functionPointer = target->second;
QObject::connect(s, &QShortcut::activated, parent,
@ -101,7 +101,7 @@ void HotkeyController::save()
std::shared_ptr<Hotkey> HotkeyController::getHotkeyByName(QString name)
{
for (auto &hotkey : this->hotkeys_)
for (const auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == name)
{
@ -115,7 +115,7 @@ int HotkeyController::replaceHotkey(QString oldName,
std::shared_ptr<Hotkey> newHotkey)
{
int i = 0;
for (auto &hotkey : this->hotkeys_)
for (const auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == oldName)
{
@ -544,7 +544,7 @@ void HotkeyController::tryAddDefault(std::set<QString> &addedHotkeys,
void HotkeyController::showHotkeyError(const std::shared_ptr<Hotkey> &hotkey,
QString warning)
{
auto msgBox = new QMessageBox(
auto *msgBox = new QMessageBox(
QMessageBox::Icon::Warning, "Hotkey error",
QString(
"There was an error while executing your hotkey named \"%1\": \n%2")

View file

@ -32,9 +32,11 @@ bool isIgnoredMessage(IgnoredMessageParameters &&params)
{
auto sourceUserID = params.twitchUserID;
bool isBlocked =
getApp()->accounts->twitch.getCurrent()->blockedUserIds().contains(
sourceUserID);
bool isBlocked = getIApp()
->getAccounts()
->twitch.getCurrent()
->blockedUserIds()
.contains(sourceUserID);
if (isBlocked)
{
switch (static_cast<ShowIgnoredUsersMessages>(

View file

@ -143,12 +143,16 @@ const std::optional<ImagePtr> &ModerationAction::getImage() const
if (this->imageToLoad_ != 0)
{
if (this->imageToLoad_ == 1)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->imageToLoad_ == 2)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
}
return this->image_;
}

View file

@ -26,7 +26,7 @@
namespace chatterino {
void NotificationController::initialize(Settings &settings, Paths &paths)
void NotificationController::initialize(Settings &settings, const Paths &paths)
{
this->initialized_ = true;
for (const QString &channelName : this->twitchSetting_.getValue())
@ -225,7 +225,7 @@ void NotificationController::removeFakeChannel(const QString channelName)
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
const auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{

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