mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Merge remote-tracking branch 'origin/master' into zneix/feature/qt6
This commit is contained in:
commit
e21e340b98
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,5 +1,8 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
|
- name: Issue about the Chatterino Browser Extension
|
||||||
|
url: https://github.com/Chatterino/chatterino-browser-ext/issues
|
||||||
|
about: Make a suggestion or report a bug about the Chatterino browser extension.
|
||||||
- name: Suggestions or feature request
|
- name: Suggestions or feature request
|
||||||
url: https://github.com/chatterino/chatterino2/discussions/categories/ideas
|
url: https://github.com/chatterino/chatterino2/discussions/categories/ideas
|
||||||
about: Got something you think should change or be added? Search for or start a new discussion!
|
about: Got something you think should change or be added? Search for or start a new discussion!
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
name: Issue about the Chatterino Browser Extension
|
|
||||||
about: Make a suggestion or report a bug about the Chatterino browser extension.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Issues for the extension are tracked here: https://github.com/chatterino/chatterino-browser-ext
|
|
2
.github/workflows/changelog-check.yml
vendored
2
.github/workflows/changelog-check.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
|
|
||||||
# Gives an error if there's no change in the changelog (except using label)
|
# Gives an error if there's no change in the changelog (except using label)
|
||||||
- name: Changelog check
|
- name: Changelog check
|
||||||
uses: dangoslen/changelog-enforcer@v2.2.0
|
uses: dangoslen/changelog-enforcer@v2.3.1
|
||||||
with:
|
with:
|
||||||
changeLogPath: 'CHANGELOG.md'
|
changeLogPath: 'CHANGELOG.md'
|
||||||
skipLabels: 'no changelog entry needed, ci, submodules'
|
skipLabels: 'no changelog entry needed, ci, submodules'
|
||||||
|
|
|
@ -33,7 +33,7 @@ Note: This installation will take about 1.5 GB of disk space.
|
||||||
|
|
||||||
### For our websocket library, we need OpenSSL 1.1
|
### For our websocket library, we need OpenSSL 1.1
|
||||||
|
|
||||||
1. Download OpenSSL for windows, version `1.1.1k`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1k.exe)**
|
1. Download OpenSSL for windows, version `1.1.1l`: **[Download](https://slproweb.com/download/Win64OpenSSL-1_1_1L.exe)**
|
||||||
2. When prompted, install OpenSSL to `C:\local\openssl`
|
2. When prompted, install OpenSSL to `C:\local\openssl`
|
||||||
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
3. When prompted, copy the OpenSSL DLLs to "The OpenSSL binaries (/bin) directory".
|
||||||
|
|
||||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,11 +2,31 @@
|
||||||
|
|
||||||
## Unversioned
|
## Unversioned
|
||||||
|
|
||||||
|
- Minor: Add `{channel.name}`, `{channel.id}`, `{stream.game}`, `{stream.title}`, `{my.id}`, `{my.name}` placeholders for commands (#3155)
|
||||||
- Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136)
|
- Minor: Remove TwitchEmotes.com attribution and the open/copy options when right-clicking a Twitch Emote. (#2214, #3136)
|
||||||
- Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143)
|
- Minor: Strip leading @ and trailing , from username in /user and /usercard commands. (#3143)
|
||||||
- Minor: Display a system message when reloading subscription emotes to match BTTV/FFZ behavior (#3135)
|
- Minor: Display a system message when reloading subscription emotes to match BTTV/FFZ behavior (#3135)
|
||||||
|
- Minor: Allow resub messages to show in `/mentions` tab (#3148)
|
||||||
|
- Minor: Added a setting to hide similar messages by any user. (#2716)
|
||||||
|
- Minor: Duplicate spaces now count towards the display message length. (#3002)
|
||||||
|
- Minor: Commands are now backed up. (#3168)
|
||||||
|
- Minor: Added the ability to open an entire tab as a popup. (#3082)
|
||||||
|
- Minor: Added optional parameter to /usercard command for opening a usercard in a different channel context. (#3172)
|
||||||
|
- Minor: Added regex option to Nicknames. (#3146)
|
||||||
|
- Minor: Added `/raw` command. (#3189)
|
||||||
|
- Minor: Colorizing usernames on IRC, originally made for Mm2PL/dankerino (#3206)
|
||||||
|
- Minor: Fixed `/streamlink` command not stripping leading @'s or #'s (#3215)
|
||||||
|
- Minor: Strip leading @ and trailing , from username in `/popout` command. (#3217)
|
||||||
|
- Minor: Added `flags.reward_message` filter variable (#3231)
|
||||||
|
- Bugfix: Fixed colored usernames sometimes not working. (#3170)
|
||||||
|
- Bugfix: Restored ability to send duplicate `/me` messages. (#3166)
|
||||||
|
- Bugfix: Notifications for moderators about other moderators deleting messages can now be disabled. (#3121)
|
||||||
- Bugfix: Moderation mode and active filters are now preserved when opening a split as a popup. (#3113, #3130)
|
- Bugfix: Moderation mode and active filters are now preserved when opening a split as a popup. (#3113, #3130)
|
||||||
- Bugfix: Fixed a bug that caused all badge highlights to use the same color. (#3132, #3134)
|
- Bugfix: Fixed a bug that caused all badge highlights to use the same color. (#3132, #3134)
|
||||||
|
- Bugfix: Allow starting Streamlink from Chatterino when running as a Flatpak. (#3178)
|
||||||
|
- Bugfix: Fixed own IRC messages not having metadata and a link to a usercard. (#3203)
|
||||||
|
- Bugfix: Fixed some channels still not loading in rare cases. (#3219)
|
||||||
|
- Bugfix: Fixed a bug with usernames or emotes completing from the wrong position. (#3229)
|
||||||
- Dev: Renamed CMake's build option `USE_SYSTEM_QT5KEYCHAIN` to `USE_SYSTEM_QTKEYCHAIN`. (#3103)
|
- Dev: Renamed CMake's build option `USE_SYSTEM_QT5KEYCHAIN` to `USE_SYSTEM_QTKEYCHAIN`. (#3103)
|
||||||
- Dev: Add benchmarks that can be compiled with the `BUILD_BENCHMARKS` CMake flag. Off by default. (#3038)
|
- Dev: Add benchmarks that can be compiled with the `BUILD_BENCHMARKS` CMake flag. Off by default. (#3038)
|
||||||
|
|
||||||
|
|
|
@ -215,3 +215,28 @@ Keep the element on the stack if possible. If you need a pointer or have complex
|
||||||
- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetimes where possible. Objects are destroyed when their parent object is destroyed.
|
- Use the [object tree](https://doc.qt.io/qt-5/objecttrees.html#) to manage lifetimes where possible. Objects are destroyed when their parent object is destroyed.
|
||||||
- If you have to explicitly delete an object use `variable->deleteLater()` instead of `delete variable`. This ensures that it will be deleted on the correct thread.
|
- If you have to explicitly delete an object use `variable->deleteLater()` instead of `delete variable`. This ensures that it will be deleted on the correct thread.
|
||||||
- If an object doesn't have a parent, consider using `std::unique_ptr<Type, DeleteLater>` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope, or the object is destroyed.
|
- If an object doesn't have a parent, consider using `std::unique_ptr<Type, DeleteLater>` with `DeleteLater` from "src/common/Common.hpp". This will call `deleteLater()` on the pointer once it goes out of scope, or the object is destroyed.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
#### Usage strings
|
||||||
|
|
||||||
|
When informing the user about how a command is supposed to be used, we aim to follow [this standard](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) where possible.
|
||||||
|
|
||||||
|
- Square brackets are reserved for `[optional arguments]`.
|
||||||
|
- Angle brackets are reserved for `<required arguments>`.
|
||||||
|
- The word _Usage_ should be capitalized and must be followed by a colon.
|
||||||
|
- If the usage deserves a description, put a dot after all parameters and explain it briefly.
|
||||||
|
|
||||||
|
##### Good
|
||||||
|
|
||||||
|
- `Usage: /block <user>`
|
||||||
|
- `Usage: /unblock <user>. Unblocks a user.`
|
||||||
|
- `Usage: /streamlink <channel>`
|
||||||
|
- `Usage: /usercard <user> [channel]`
|
||||||
|
|
||||||
|
##### Bad
|
||||||
|
|
||||||
|
- `Usage /streamlink <channel>` - Missing colon after _Usage_.
|
||||||
|
- `usage: /streamlink <channel>` - _Usage_ must be capitalized.
|
||||||
|
- `Usage: /streamlink channel` - The required argument `channel` must be wrapped in angle brackets.
|
||||||
|
- `Usage: /streamlink <channel>.` - Don't put a dot after usage if it's not followed by a description.
|
||||||
|
|
|
@ -167,8 +167,6 @@ SOURCES += \
|
||||||
src/controllers/notifications/NotificationController.cpp \
|
src/controllers/notifications/NotificationController.cpp \
|
||||||
src/controllers/notifications/NotificationModel.cpp \
|
src/controllers/notifications/NotificationModel.cpp \
|
||||||
src/controllers/pings/MutedChannelModel.cpp \
|
src/controllers/pings/MutedChannelModel.cpp \
|
||||||
src/controllers/taggedusers/TaggedUser.cpp \
|
|
||||||
src/controllers/taggedusers/TaggedUsersModel.cpp \
|
|
||||||
src/debug/Benchmark.cpp \
|
src/debug/Benchmark.cpp \
|
||||||
src/main.cpp \
|
src/main.cpp \
|
||||||
src/messages/Emote.cpp \
|
src/messages/Emote.cpp \
|
||||||
|
@ -400,8 +398,6 @@ HEADERS += \
|
||||||
src/controllers/notifications/NotificationController.hpp \
|
src/controllers/notifications/NotificationController.hpp \
|
||||||
src/controllers/notifications/NotificationModel.hpp \
|
src/controllers/notifications/NotificationModel.hpp \
|
||||||
src/controllers/pings/MutedChannelModel.hpp \
|
src/controllers/pings/MutedChannelModel.hpp \
|
||||||
src/controllers/taggedusers/TaggedUser.hpp \
|
|
||||||
src/controllers/taggedusers/TaggedUsersModel.hpp \
|
|
||||||
src/debug/AssertInGuiThread.hpp \
|
src/debug/AssertInGuiThread.hpp \
|
||||||
src/debug/Benchmark.hpp \
|
src/debug/Benchmark.hpp \
|
||||||
src/ForwardDecl.hpp \
|
src/ForwardDecl.hpp \
|
||||||
|
|
|
@ -35,6 +35,7 @@ public:
|
||||||
lru_cache(lru_cache<key_t, value_t> &&other)
|
lru_cache(lru_cache<key_t, value_t> &&other)
|
||||||
: _cache_items_list(std::move(other._cache_items_list))
|
: _cache_items_list(std::move(other._cache_items_list))
|
||||||
, _cache_items_map(std::move(other._cache_items_map))
|
, _cache_items_map(std::move(other._cache_items_map))
|
||||||
|
, _max_size(other._max_size)
|
||||||
{
|
{
|
||||||
other._cache_items_list.clear();
|
other._cache_items_list.clear();
|
||||||
other._cache_items_map.clear();
|
other._cache_items_map.clear();
|
||||||
|
@ -44,6 +45,7 @@ public:
|
||||||
{
|
{
|
||||||
_cache_items_list = std::move(other._cache_items_list);
|
_cache_items_list = std::move(other._cache_items_list);
|
||||||
_cache_items_map = std::move(other._cache_items_map);
|
_cache_items_map = std::move(other._cache_items_map);
|
||||||
|
_max_size = other._max_size;
|
||||||
other._cache_items_list.clear();
|
other._cache_items_list.clear();
|
||||||
other._cache_items_map.clear();
|
other._cache_items_map.clear();
|
||||||
return *this;
|
return *this;
|
||||||
|
|
|
@ -286,7 +286,7 @@ void Application::initPubsub()
|
||||||
auto chan =
|
auto chan =
|
||||||
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
this->twitch.server->getChannelOrEmptyByID(action.roomID);
|
||||||
|
|
||||||
if (chan->isEmpty())
|
if (chan->isEmpty() || getSettings()->hideDeletionActions)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,11 +110,6 @@ set(SOURCE_FILES
|
||||||
controllers/pings/MutedChannelModel.cpp
|
controllers/pings/MutedChannelModel.cpp
|
||||||
controllers/pings/MutedChannelModel.hpp
|
controllers/pings/MutedChannelModel.hpp
|
||||||
|
|
||||||
controllers/taggedusers/TaggedUser.cpp
|
|
||||||
controllers/taggedusers/TaggedUser.hpp
|
|
||||||
controllers/taggedusers/TaggedUsersModel.cpp
|
|
||||||
controllers/taggedusers/TaggedUsersModel.hpp
|
|
||||||
|
|
||||||
debug/Benchmark.cpp
|
debug/Benchmark.cpp
|
||||||
debug/Benchmark.hpp
|
debug/Benchmark.hpp
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,12 @@ void ChannelChatters::updateOnlineChatters(
|
||||||
chatters_->updateOnlineChatters(chatters);
|
chatters_->updateOnlineChatters(chatters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t ChannelChatters::colorsSize() const
|
||||||
|
{
|
||||||
|
auto size = this->chatterColors_.access()->size();
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
const QColor ChannelChatters::getUserColor(const QString &user)
|
const QColor ChannelChatters::getUserColor(const QString &user)
|
||||||
{
|
{
|
||||||
const auto chatterColors = this->chatterColors_.access();
|
const auto chatterColors = this->chatterColors_.access();
|
||||||
|
|
|
@ -25,9 +25,13 @@ public:
|
||||||
void setUserColor(const QString &user, const QColor &color);
|
void setUserColor(const QString &user, const QColor &color);
|
||||||
void updateOnlineChatters(const std::unordered_set<QString> &chatters);
|
void updateOnlineChatters(const std::unordered_set<QString> &chatters);
|
||||||
|
|
||||||
private:
|
// colorsSize returns the amount of colors stored in `chatterColors_`
|
||||||
|
// NOTE: This function is only meant to be used in tests and benchmarks
|
||||||
|
size_t colorsSize() const;
|
||||||
|
|
||||||
static constexpr int maxChatterColorCount = 5000;
|
static constexpr int maxChatterColorCount = 5000;
|
||||||
|
|
||||||
|
private:
|
||||||
Channel &channel_;
|
Channel &channel_;
|
||||||
|
|
||||||
// maps 2 char prefix to set of names
|
// maps 2 char prefix to set of names
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
#include "common/Modes.hpp"
|
#include "common/Modes.hpp"
|
||||||
|
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
#define UGLYMACROHACK1(s) #s
|
#define UGLYMACROHACK1(s) #s
|
||||||
#define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s)
|
#define FROM_EXTERNAL_DEFINE(s) UGLYMACROHACK1(s)
|
||||||
|
|
||||||
|
@ -71,4 +73,9 @@ const bool &Version::isSupportedOS() const
|
||||||
return this->isSupportedOS_;
|
return this->isSupportedOS_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Version::isFlatpak() const
|
||||||
|
{
|
||||||
|
return QFileInfo::exists("/.flatpak-info");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -29,6 +29,7 @@ public:
|
||||||
const QString &dateOfBuild() const;
|
const QString &dateOfBuild() const;
|
||||||
const QString &fullVersion() const;
|
const QString &fullVersion() const;
|
||||||
const bool &isSupportedOS() const;
|
const bool &isSupportedOS() const;
|
||||||
|
bool isFlatpak() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Version();
|
Version();
|
||||||
|
|
|
@ -110,6 +110,42 @@ void SplitDescriptor::loadFromJSON(SplitDescriptor &descriptor,
|
||||||
descriptor.filters_ = loadFilters(root.value("filters"));
|
descriptor.filters_ = loadFilters(root.value("filters"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TabDescriptor TabDescriptor::loadFromJSON(const QJsonObject &tabObj)
|
||||||
|
{
|
||||||
|
TabDescriptor tab;
|
||||||
|
// Load tab custom title
|
||||||
|
QJsonValue titleVal = tabObj.value("title");
|
||||||
|
if (titleVal.isString())
|
||||||
|
{
|
||||||
|
tab.customTitle_ = titleVal.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load tab selected state
|
||||||
|
tab.selected_ = tabObj.value("selected").toBool(false);
|
||||||
|
|
||||||
|
// Load tab "highlightsEnabled" state
|
||||||
|
tab.highlightsEnabled_ = tabObj.value("highlightsEnabled").toBool(true);
|
||||||
|
|
||||||
|
QJsonObject splitRoot = tabObj.value("splits2").toObject();
|
||||||
|
|
||||||
|
// Load tab splits
|
||||||
|
if (!splitRoot.isEmpty())
|
||||||
|
{
|
||||||
|
// root type
|
||||||
|
auto nodeType = splitRoot.value("type").toString();
|
||||||
|
if (nodeType == "split")
|
||||||
|
{
|
||||||
|
tab.rootNode_ = loadNodes<SplitNodeDescriptor>(splitRoot);
|
||||||
|
}
|
||||||
|
else if (nodeType == "horizontal" || nodeType == "vertical")
|
||||||
|
{
|
||||||
|
tab.rootNode_ = loadNodes<ContainerNodeDescriptor>(splitRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
WindowLayout WindowLayout::loadFromFile(const QString &path)
|
WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
{
|
{
|
||||||
WindowLayout layout;
|
WindowLayout layout;
|
||||||
|
@ -117,15 +153,15 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
bool hasSetAMainWindow = false;
|
bool hasSetAMainWindow = false;
|
||||||
|
|
||||||
// "deserialize"
|
// "deserialize"
|
||||||
for (const QJsonValue &window_val : loadWindowArray(path))
|
for (const QJsonValue &windowVal : loadWindowArray(path))
|
||||||
{
|
{
|
||||||
QJsonObject window_obj = window_val.toObject();
|
QJsonObject windowObj = windowVal.toObject();
|
||||||
|
|
||||||
WindowDescriptor window;
|
WindowDescriptor window;
|
||||||
|
|
||||||
// Load window type
|
// Load window type
|
||||||
QString type_val = window_obj.value("type").toString();
|
QString typeVal = windowObj.value("type").toString();
|
||||||
auto type = type_val == "main" ? WindowType::Main : WindowType::Popup;
|
auto type = typeVal == "main" ? WindowType::Main : WindowType::Popup;
|
||||||
|
|
||||||
if (type == WindowType::Main)
|
if (type == WindowType::Main)
|
||||||
{
|
{
|
||||||
|
@ -142,21 +178,21 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
window.type_ = type;
|
window.type_ = type;
|
||||||
|
|
||||||
// Load window state
|
// Load window state
|
||||||
if (window_obj.value("state") == "minimized")
|
if (windowObj.value("state") == "minimized")
|
||||||
{
|
{
|
||||||
window.state_ = WindowDescriptor::State::Minimized;
|
window.state_ = WindowDescriptor::State::Minimized;
|
||||||
}
|
}
|
||||||
else if (window_obj.value("state") == "maximized")
|
else if (windowObj.value("state") == "maximized")
|
||||||
{
|
{
|
||||||
window.state_ = WindowDescriptor::State::Maximized;
|
window.state_ = WindowDescriptor::State::Maximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load window geometry
|
// Load window geometry
|
||||||
{
|
{
|
||||||
int x = window_obj.value("x").toInt(-1);
|
int x = windowObj.value("x").toInt(-1);
|
||||||
int y = window_obj.value("y").toInt(-1);
|
int y = windowObj.value("y").toInt(-1);
|
||||||
int width = window_obj.value("width").toInt(-1);
|
int width = windowObj.value("width").toInt(-1);
|
||||||
int height = window_obj.value("height").toInt(-1);
|
int height = windowObj.value("height").toInt(-1);
|
||||||
|
|
||||||
window.geometry_ = QRect(x, y, width, height);
|
window.geometry_ = QRect(x, y, width, height);
|
||||||
}
|
}
|
||||||
|
@ -164,23 +200,10 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
bool hasSetASelectedTab = false;
|
bool hasSetASelectedTab = false;
|
||||||
|
|
||||||
// Load window tabs
|
// Load window tabs
|
||||||
QJsonArray tabs = window_obj.value("tabs").toArray();
|
QJsonArray tabs = windowObj.value("tabs").toArray();
|
||||||
for (QJsonValue tab_val : tabs)
|
for (QJsonValue tabVal : tabs)
|
||||||
{
|
{
|
||||||
TabDescriptor tab;
|
TabDescriptor tab = TabDescriptor::loadFromJSON(tabVal.toObject());
|
||||||
|
|
||||||
QJsonObject tab_obj = tab_val.toObject();
|
|
||||||
|
|
||||||
// Load tab custom title
|
|
||||||
QJsonValue title_val = tab_obj.value("title");
|
|
||||||
if (title_val.isString())
|
|
||||||
{
|
|
||||||
tab.customTitle_ = title_val.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tab selected state
|
|
||||||
tab.selected_ = tab_obj.value("selected").toBool(false);
|
|
||||||
|
|
||||||
if (tab.selected_)
|
if (tab.selected_)
|
||||||
{
|
{
|
||||||
if (hasSetASelectedTab)
|
if (hasSetASelectedTab)
|
||||||
|
@ -192,34 +215,11 @@ WindowLayout WindowLayout::loadFromFile(const QString &path)
|
||||||
}
|
}
|
||||||
hasSetASelectedTab = true;
|
hasSetASelectedTab = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load tab "highlightsEnabled" state
|
|
||||||
tab.highlightsEnabled_ =
|
|
||||||
tab_obj.value("highlightsEnabled").toBool(true);
|
|
||||||
|
|
||||||
QJsonObject splitRoot = tab_obj.value("splits2").toObject();
|
|
||||||
|
|
||||||
// Load tab splits
|
|
||||||
if (!splitRoot.isEmpty())
|
|
||||||
{
|
|
||||||
// root type
|
|
||||||
auto nodeType = splitRoot.value("type").toString();
|
|
||||||
if (nodeType == "split")
|
|
||||||
{
|
|
||||||
tab.rootNode_ = loadNodes<SplitNodeDescriptor>(splitRoot);
|
|
||||||
}
|
|
||||||
else if (nodeType == "horizontal" || nodeType == "vertical")
|
|
||||||
{
|
|
||||||
tab.rootNode_ =
|
|
||||||
loadNodes<ContainerNodeDescriptor>(splitRoot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.tabs_.emplace_back(std::move(tab));
|
window.tabs_.emplace_back(std::move(tab));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load emote popup position
|
// Load emote popup position
|
||||||
QJsonObject emote_popup_obj = window_obj.value("emotePopup").toObject();
|
QJsonObject emote_popup_obj = windowObj.value("emotePopup").toObject();
|
||||||
layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(),
|
layout.emotePopupPos_ = QPoint(emote_popup_obj.value("x").toInt(),
|
||||||
emote_popup_obj.value("y").toInt());
|
emote_popup_obj.value("y").toInt());
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,8 @@ struct ContainerNodeDescriptor {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TabDescriptor {
|
struct TabDescriptor {
|
||||||
|
static TabDescriptor loadFromJSON(const QJsonObject &root);
|
||||||
|
|
||||||
QString customTitle_;
|
QString customTitle_;
|
||||||
bool selected_{false};
|
bool selected_{false};
|
||||||
bool highlightsEnabled_{true};
|
bool highlightsEnabled_{true};
|
||||||
|
|
|
@ -226,6 +226,72 @@ bool appendWhisperMessageStringLocally(const QString &textNoEmoji)
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::map<QString,
|
||||||
|
std::function<QString(const QString &, const ChannelPtr &)>>
|
||||||
|
COMMAND_VARS{
|
||||||
|
{
|
||||||
|
"channel.name",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
(void)(altText); //unused
|
||||||
|
return channel->getName();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel.id",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
||||||
|
if (tc == nullptr)
|
||||||
|
{
|
||||||
|
return altText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tc->roomId();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stream.game",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
||||||
|
if (tc == nullptr)
|
||||||
|
{
|
||||||
|
return altText;
|
||||||
|
}
|
||||||
|
const auto &status = tc->accessStreamStatus();
|
||||||
|
return status->live ? status->game : altText;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stream.title",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
auto *tc = dynamic_cast<TwitchChannel *>(channel.get());
|
||||||
|
if (tc == nullptr)
|
||||||
|
{
|
||||||
|
return altText;
|
||||||
|
}
|
||||||
|
const auto &status = tc->accessStreamStatus();
|
||||||
|
return status->live ? status->title : altText;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"my.id",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
(void)(channel); //unused
|
||||||
|
auto uid = getApp()->accounts->twitch.getCurrent()->getUserId();
|
||||||
|
return uid.isEmpty() ? altText : uid;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"my.name",
|
||||||
|
[](const auto &altText, const auto &channel) {
|
||||||
|
(void)(channel); //unused
|
||||||
|
auto name =
|
||||||
|
getApp()->accounts->twitch.getCurrent()->getUserName();
|
||||||
|
return name.isEmpty() ? altText : name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -267,6 +333,8 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
auto path = combinePath(paths.settingsDirectory, "commands.json");
|
auto path = combinePath(paths.settingsDirectory, "commands.json");
|
||||||
this->sm_ = std::make_shared<pajlada::Settings::SettingManager>();
|
this->sm_ = std::make_shared<pajlada::Settings::SettingManager>();
|
||||||
this->sm_->setPath(path.toStdString());
|
this->sm_->setPath(path.toStdString());
|
||||||
|
this->sm_->setBackupEnabled(true);
|
||||||
|
this->sm_->setBackupSlots(9);
|
||||||
|
|
||||||
// Delayed initialization of the setting storing all commands
|
// Delayed initialization of the setting storing all commands
|
||||||
this->commandsSetting_.reset(
|
this->commandsSetting_.reset(
|
||||||
|
@ -294,7 +362,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
auto blockLambda = [](const auto &words, auto channel) {
|
auto blockLambda = [](const auto &words, auto channel) {
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage("Usage: /block [user]"));
|
channel->addMessage(makeSystemMessage("Usage: /block <user>"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +407,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
auto unblockLambda = [](const auto &words, auto channel) {
|
auto unblockLambda = [](const auto &words, auto channel) {
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage("Usage: /unblock [user]"));
|
channel->addMessage(makeSystemMessage("Usage: /unblock <user>"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +521,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(
|
channel->addMessage(
|
||||||
makeSystemMessage("Usage /user [user] (channel)"));
|
makeSystemMessage("Usage: /user <user> [channel]"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
QString userName = words[1];
|
QString userName = words[1];
|
||||||
|
@ -474,12 +542,33 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
this->registerCommand("/usercard", [](const auto &words, auto channel) {
|
this->registerCommand("/usercard", [](const auto &words, auto channel) {
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage("Usage /usercard [user]"));
|
channel->addMessage(
|
||||||
|
makeSystemMessage("Usage: /usercard <user> [channel]"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
QString userName = words[1];
|
QString userName = words[1];
|
||||||
stripUserName(userName);
|
stripUserName(userName);
|
||||||
|
|
||||||
|
if (words.size() > 2)
|
||||||
|
{
|
||||||
|
QString channelName = words[2];
|
||||||
|
stripChannelName(channelName);
|
||||||
|
|
||||||
|
ChannelPtr channelTemp =
|
||||||
|
getApp()->twitch2->getChannelOrEmpty(channelName);
|
||||||
|
|
||||||
|
if (channelTemp->isEmpty())
|
||||||
|
{
|
||||||
|
channel->addMessage(makeSystemMessage(
|
||||||
|
"A usercard can only be displayed for a channel that is "
|
||||||
|
"currently opened in Chatterino."));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = channelTemp;
|
||||||
|
}
|
||||||
|
|
||||||
auto *userPopup = new UserInfoPopup(
|
auto *userPopup = new UserInfoPopup(
|
||||||
getSettings()->autoCloseUserPopup,
|
getSettings()->autoCloseUserPopup,
|
||||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())));
|
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())));
|
||||||
|
@ -602,12 +691,13 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
(!channel->isTwitchChannel() || channel->isEmpty()))
|
(!channel->isTwitchChannel() || channel->isEmpty()))
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage(
|
channel->addMessage(makeSystemMessage(
|
||||||
"Usage: /streamlink [channel]. You can also use the "
|
"Usage: /streamlink <channel>. You can also use the "
|
||||||
"command without arguments in any Twitch channel to open "
|
"command without arguments in any Twitch channel to open "
|
||||||
"it in streamlink."));
|
"it in streamlink."));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stripChannelName(target);
|
||||||
channel->addMessage(makeSystemMessage(
|
channel->addMessage(makeSystemMessage(
|
||||||
QString("Opening %1 in streamlink...").arg(target)));
|
QString("Opening %1 in streamlink...").arg(target)));
|
||||||
openStreamlinkForChannel(target);
|
openStreamlinkForChannel(target);
|
||||||
|
@ -623,12 +713,13 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
(!channel->isTwitchChannel() || channel->isEmpty()))
|
(!channel->isTwitchChannel() || channel->isEmpty()))
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage(
|
channel->addMessage(makeSystemMessage(
|
||||||
"Usage: /popout [channel]. You can also use the command "
|
"Usage: /popout <channel>. You can also use the command "
|
||||||
"without arguments in any Twitch channel to open its "
|
"without arguments in any Twitch channel to open its "
|
||||||
"popout chat."));
|
"popout chat."));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stripChannelName(target);
|
||||||
QDesktopServices::openUrl(
|
QDesktopServices::openUrl(
|
||||||
QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=")
|
QUrl(QString("https://www.twitch.tv/popout/%1/chat?popout=")
|
||||||
.arg(target)));
|
.arg(target)));
|
||||||
|
@ -650,7 +741,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(
|
channel->addMessage(
|
||||||
makeSystemMessage("Usage: /settitle <stream title>."));
|
makeSystemMessage("Usage: /settitle <stream title>"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||||
|
@ -681,7 +772,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(
|
channel->addMessage(
|
||||||
makeSystemMessage("Usage: /setgame <stream game>."));
|
makeSystemMessage("Usage: /setgame <stream game>"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
|
||||||
|
@ -746,7 +837,7 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
const ChannelPtr channel) {
|
const ChannelPtr channel) {
|
||||||
if (words.size() < 2)
|
if (words.size() < 2)
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage("Usage: /openurl <URL>."));
|
channel->addMessage(makeSystemMessage("Usage: /openurl <URL>"));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,6 +865,11 @@ void CommandController::initialize(Settings &, Paths &paths)
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this->registerCommand("/raw", [](const QStringList &words, ChannelPtr) {
|
||||||
|
getApp()->twitch2->sendRawMessage(words.mid(1).join(" "));
|
||||||
|
return "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandController::save()
|
void CommandController::save()
|
||||||
|
@ -825,7 +921,7 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
||||||
if (it != this->userCommands_.end())
|
if (it != this->userCommands_.end())
|
||||||
{
|
{
|
||||||
text = getApp()->emotes->emojis.replaceShortCodes(
|
text = getApp()->emotes->emojis.replaceShortCodes(
|
||||||
this->execCustomCommand(words, it.value(), dryRun));
|
this->execCustomCommand(words, it.value(), dryRun, channel));
|
||||||
|
|
||||||
words = text.split(' ', Qt::SkipEmptyParts);
|
words = text.split(' ', Qt::SkipEmptyParts);
|
||||||
|
|
||||||
|
@ -858,7 +954,7 @@ QString CommandController::execCommand(const QString &textNoEmoji,
|
||||||
const auto it = this->userCommands_.find(commandName);
|
const auto it = this->userCommands_.find(commandName);
|
||||||
if (it != this->userCommands_.end())
|
if (it != this->userCommands_.end())
|
||||||
{
|
{
|
||||||
return this->execCustomCommand(words, it.value(), dryRun);
|
return this->execCustomCommand(words, it.value(), dryRun, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -877,11 +973,13 @@ void CommandController::registerCommand(QString commandName,
|
||||||
|
|
||||||
QString CommandController::execCustomCommand(const QStringList &words,
|
QString CommandController::execCustomCommand(const QStringList &words,
|
||||||
const Command &command,
|
const Command &command,
|
||||||
bool dryRun)
|
bool dryRun, ChannelPtr channel,
|
||||||
|
std::map<QString, QString> context)
|
||||||
{
|
{
|
||||||
QString result;
|
QString result;
|
||||||
|
|
||||||
static QRegularExpression parseCommand("(^|[^{])({{)*{(\\d+\\+?)}");
|
static QRegularExpression parseCommand(
|
||||||
|
R"((^|[^{])({{)*{(\d+\+?|([a-zA-Z.-]+)(?:;(.+?))?)})");
|
||||||
|
|
||||||
int lastCaptureEnd = 0;
|
int lastCaptureEnd = 0;
|
||||||
|
|
||||||
|
@ -912,8 +1010,28 @@ QString CommandController::execCustomCommand(const QStringList &words,
|
||||||
bool ok;
|
bool ok;
|
||||||
int wordIndex = wordIndexMatch.replace("=", "").toInt(&ok);
|
int wordIndex = wordIndexMatch.replace("=", "").toInt(&ok);
|
||||||
if (!ok || wordIndex == 0)
|
if (!ok || wordIndex == 0)
|
||||||
|
{
|
||||||
|
auto varName = match.captured(4);
|
||||||
|
auto altText = match.captured(5); // alt text or empty string
|
||||||
|
|
||||||
|
auto var = COMMAND_VARS.find(varName);
|
||||||
|
|
||||||
|
if (var != COMMAND_VARS.end())
|
||||||
|
{
|
||||||
|
result += var->second(altText, channel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto it = context.find(varName);
|
||||||
|
if (it != context.end())
|
||||||
|
{
|
||||||
|
result += it->second.isEmpty() ? altText : it->second;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
result += "{" + match.captured(3) + "}";
|
result += "{" + match.captured(3) + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ public:
|
||||||
|
|
||||||
CommandModel *createModel(QObject *parent);
|
CommandModel *createModel(QObject *parent);
|
||||||
|
|
||||||
|
QString execCustomCommand(const QStringList &words, const Command &command,
|
||||||
|
bool dryRun, ChannelPtr channel,
|
||||||
|
std::map<QString, QString> context = {});
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load(Paths &paths);
|
void load(Paths &paths);
|
||||||
|
|
||||||
|
@ -57,9 +61,6 @@ private:
|
||||||
std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>>
|
std::unique_ptr<pajlada::Settings::Setting<std::vector<Command>>>
|
||||||
commandsSetting_;
|
commandsSetting_;
|
||||||
|
|
||||||
QString execCustomCommand(const QStringList &words, const Command &command,
|
|
||||||
bool dryRun);
|
|
||||||
|
|
||||||
QStringList commandAutoCompletions_;
|
QStringList commandAutoCompletions_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
* flags.points_redeemed
|
* flags.points_redeemed
|
||||||
* flags.sub_message
|
* flags.sub_message
|
||||||
* flags.system_message
|
* flags.system_message
|
||||||
|
* flags.reward_message
|
||||||
* flags.whisper
|
* flags.whisper
|
||||||
*
|
*
|
||||||
* message.content
|
* message.content
|
||||||
|
@ -77,6 +78,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
||||||
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||||
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||||
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||||
|
{"flags.reward_message",
|
||||||
|
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
|
||||||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||||
|
|
||||||
{"message.content", m->messageText},
|
{"message.content", m->messageText},
|
||||||
|
|
|
@ -22,6 +22,7 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
||||||
{"flags.points_redeemed", "redeemed points?"},
|
{"flags.points_redeemed", "redeemed points?"},
|
||||||
{"flags.sub_message", "sub/resub message?"},
|
{"flags.sub_message", "sub/resub message?"},
|
||||||
{"flags.system_message", "system message?"},
|
{"flags.system_message", "system message?"},
|
||||||
|
{"flags.reward_message", "channel point reward message?"},
|
||||||
{"flags.whisper", "whisper message?"},
|
{"flags.whisper", "whisper message?"},
|
||||||
{"message.content", "message text"},
|
{"message.content", "message text"},
|
||||||
{"message.length", "message length"}};
|
{"message.length", "message length"}};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
|
||||||
#include "util/RapidJsonSerializeQString.hpp"
|
#include "util/RapidJsonSerializeQString.hpp"
|
||||||
#include "util/RapidjsonHelpers.hpp"
|
#include "util/RapidjsonHelpers.hpp"
|
||||||
|
|
||||||
|
@ -15,24 +14,92 @@ namespace chatterino {
|
||||||
class Nickname
|
class Nickname
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Nickname(const QString &name, const QString &replace)
|
Nickname(const QString &name, const QString &replace, const bool isRegex,
|
||||||
|
const bool isCaseSensitive)
|
||||||
: name_(name)
|
: name_(name)
|
||||||
, replace_(replace)
|
, replace_(replace)
|
||||||
|
, isRegex_(isRegex)
|
||||||
|
, isCaseSensitive_(isCaseSensitive)
|
||||||
|
, caseSensitivity_(this->isCaseSensitive_ ? Qt::CaseSensitive
|
||||||
|
: Qt::CaseInsensitive)
|
||||||
{
|
{
|
||||||
|
if (this->isRegex())
|
||||||
|
{
|
||||||
|
this->regex_ = QRegularExpression(
|
||||||
|
name, QRegularExpression::UseUnicodePropertiesOption |
|
||||||
|
(this->isCaseSensitive()
|
||||||
|
? QRegularExpression::NoPatternOption
|
||||||
|
: QRegularExpression::CaseInsensitiveOption));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString &name() const
|
[[nodiscard]] const QString &name() const
|
||||||
{
|
{
|
||||||
return this->name_;
|
return this->name_;
|
||||||
}
|
}
|
||||||
const QString &replace() const
|
|
||||||
|
[[nodiscard]] const QString &replace() const
|
||||||
{
|
{
|
||||||
return this->replace_;
|
return this->replace_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool isRegex() const
|
||||||
|
{
|
||||||
|
return this->isRegex_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] Qt::CaseSensitivity caseSensitivity() const
|
||||||
|
{
|
||||||
|
return this->caseSensitivity_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const bool &isCaseSensitive() const
|
||||||
|
{
|
||||||
|
return this->isCaseSensitive_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] bool match(QString &usernameText) const
|
||||||
|
{
|
||||||
|
if (this->isRegex())
|
||||||
|
{
|
||||||
|
if (!this->regex_.isValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this->name().isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto workingCopy = usernameText;
|
||||||
|
workingCopy.replace(this->regex_, this->replace());
|
||||||
|
if (workingCopy != usernameText)
|
||||||
|
{
|
||||||
|
usernameText = workingCopy;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto res =
|
||||||
|
this->name().compare(usernameText, this->caseSensitivity());
|
||||||
|
if (res == 0)
|
||||||
|
{
|
||||||
|
usernameText = this->replace();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString name_;
|
QString name_;
|
||||||
QString replace_;
|
QString replace_;
|
||||||
|
bool isRegex_;
|
||||||
|
bool isCaseSensitive_;
|
||||||
|
Qt::CaseSensitivity caseSensitivity_;
|
||||||
|
QRegularExpression regex_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
@ -48,6 +115,8 @@ struct Serialize<chatterino::Nickname> {
|
||||||
|
|
||||||
chatterino::rj::set(ret, "name", value.name(), a);
|
chatterino::rj::set(ret, "name", value.name(), a);
|
||||||
chatterino::rj::set(ret, "replace", value.replace(), a);
|
chatterino::rj::set(ret, "replace", value.replace(), a);
|
||||||
|
chatterino::rj::set(ret, "isRegex", value.isRegex(), a);
|
||||||
|
chatterino::rj::set(ret, "isCaseSensitive", value.isCaseSensitive(), a);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -61,16 +130,21 @@ struct Deserialize<chatterino::Nickname> {
|
||||||
if (!value.IsObject())
|
if (!value.IsObject())
|
||||||
{
|
{
|
||||||
PAJLADA_REPORT_ERROR(error)
|
PAJLADA_REPORT_ERROR(error)
|
||||||
return chatterino::Nickname(QString(), QString());
|
return chatterino::Nickname(QString(), QString(), false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString _name;
|
QString _name;
|
||||||
QString _replace;
|
QString _replace;
|
||||||
|
bool _isRegex;
|
||||||
|
bool _isCaseSensitive;
|
||||||
|
|
||||||
chatterino::rj::getSafe(value, "name", _name);
|
chatterino::rj::getSafe(value, "name", _name);
|
||||||
chatterino::rj::getSafe(value, "replace", _replace);
|
chatterino::rj::getSafe(value, "replace", _replace);
|
||||||
|
chatterino::rj::getSafe(value, "isRegex", _isRegex);
|
||||||
|
chatterino::rj::getSafe(value, "isCaseSensitive", _isCaseSensitive);
|
||||||
|
|
||||||
return chatterino::Nickname(_name, _replace);
|
return chatterino::Nickname(_name, _replace, _isRegex,
|
||||||
|
_isCaseSensitive);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
NicknamesModel::NicknamesModel(QObject *parent)
|
NicknamesModel::NicknamesModel(QObject *parent)
|
||||||
: SignalVectorModel<Nickname>(2, parent)
|
: SignalVectorModel<Nickname>(4, parent)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,9 @@ Nickname NicknamesModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||||
const Nickname &original)
|
const Nickname &original)
|
||||||
{
|
{
|
||||||
return Nickname{row[0]->data(Qt::DisplayRole).toString(),
|
return Nickname{row[0]->data(Qt::DisplayRole).toString(),
|
||||||
row[1]->data(Qt::DisplayRole).toString()};
|
row[1]->data(Qt::DisplayRole).toString(),
|
||||||
|
row[2]->data(Qt::CheckStateRole).toBool(),
|
||||||
|
row[3]->data(Qt::CheckStateRole).toBool()};
|
||||||
}
|
}
|
||||||
|
|
||||||
// turns a row in the model into a vector item
|
// turns a row in the model into a vector item
|
||||||
|
@ -26,6 +28,8 @@ void NicknamesModel::getRowFromItem(const Nickname &item,
|
||||||
{
|
{
|
||||||
setStringItem(row[0], item.name());
|
setStringItem(row[0], item.name());
|
||||||
setStringItem(row[1], item.replace());
|
setStringItem(row[1], item.replace());
|
||||||
|
setBoolItem(row[2], item.isRegex());
|
||||||
|
setBoolItem(row[3], item.isCaseSensitive());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
#include "TaggedUser.hpp"
|
|
||||||
|
|
||||||
#include <tuple>
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
TaggedUser::TaggedUser(ProviderId provider, const QString &name,
|
|
||||||
const QString &id)
|
|
||||||
: providerId_(provider)
|
|
||||||
, name_(name)
|
|
||||||
, id_(id)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TaggedUser::operator<(const TaggedUser &other) const
|
|
||||||
{
|
|
||||||
return std::tie(this->providerId_, this->name_, this->id_) <
|
|
||||||
std::tie(other.providerId_, other.name_, other.id_);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProviderId TaggedUser::getProviderId() const
|
|
||||||
{
|
|
||||||
return this->providerId_;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaggedUser::getName() const
|
|
||||||
{
|
|
||||||
return this->name_;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString TaggedUser::getId() const
|
|
||||||
{
|
|
||||||
return this->id_;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace chatterino
|
|
|
@ -1,26 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "common/ProviderId.hpp"
|
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
class TaggedUser
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
TaggedUser(ProviderId providerId, const QString &name, const QString &id);
|
|
||||||
|
|
||||||
bool operator<(const TaggedUser &other) const;
|
|
||||||
|
|
||||||
ProviderId getProviderId() const;
|
|
||||||
QString getName() const;
|
|
||||||
QString getId() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
ProviderId providerId_;
|
|
||||||
QString name_;
|
|
||||||
QString id_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace chatterino
|
|
|
@ -1,67 +0,0 @@
|
||||||
#include "TaggedUsersModel.hpp"
|
|
||||||
|
|
||||||
#include "Application.hpp"
|
|
||||||
#include "util/StandardItemHelper.hpp"
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
// commandmodel
|
|
||||||
TaggedUsersModel::TaggedUsersModel(QObject *parent)
|
|
||||||
: SignalVectorModel<TaggedUser>(1, parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
// turn a vector item into a model row
|
|
||||||
TaggedUser TaggedUsersModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
|
||||||
const TaggedUser &original)
|
|
||||||
{
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
// turns a row in the model into a vector item
|
|
||||||
void TaggedUsersModel::getRowFromItem(const TaggedUser &item,
|
|
||||||
std::vector<QStandardItem *> &row)
|
|
||||||
{
|
|
||||||
setStringItem(row[0], item.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
void TaggedUsersModel::afterInit()
|
|
||||||
{
|
|
||||||
// std::vector<QStandardItem *> row = this->createRow();
|
|
||||||
// setBoolItem(row[0],
|
|
||||||
// getSettings()->enableHighlightsSelf.getValue(), true, false);
|
|
||||||
// row[0]->setData("Your username (automatic)", Qt::DisplayRole);
|
|
||||||
// setBoolItem(row[1],
|
|
||||||
// getSettings()->enableHighlightTaskbar.getValue(), true, false);
|
|
||||||
// setBoolItem(row[2],
|
|
||||||
// getSettings()->enableHighlightSound.getValue(), true, false);
|
|
||||||
// row[3]->setFlags(0); this->insertCustomRow(row, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// void TaggedUserModel::customRowSetData(const std::vector<QStandardItem *>
|
|
||||||
// &row, int column,
|
|
||||||
// const QVariant &value, int role)
|
|
||||||
//{
|
|
||||||
// switch (column) {
|
|
||||||
// case 0: {
|
|
||||||
// if (role == Qt::CheckStateRole) {
|
|
||||||
// getSettings()->enableHighlightsSelf.setValue(value.toBool());
|
|
||||||
// }
|
|
||||||
// } break;
|
|
||||||
// case 1: {
|
|
||||||
// if (role == Qt::CheckStateRole) {
|
|
||||||
// getSettings()->enableHighlightTaskbar.setValue(value.toBool());
|
|
||||||
// }
|
|
||||||
// } break;
|
|
||||||
// case 2: {
|
|
||||||
// if (role == Qt::CheckStateRole) {
|
|
||||||
// getSettings()->enableHighlightSound.setValue(value.toBool());
|
|
||||||
// }
|
|
||||||
// } break;
|
|
||||||
// case 3: {
|
|
||||||
// // empty element
|
|
||||||
// } break;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
} // namespace chatterino
|
|
|
@ -1,33 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "common/SignalVectorModel.hpp"
|
|
||||||
#include "controllers/taggedusers/TaggedUser.hpp"
|
|
||||||
|
|
||||||
namespace chatterino {
|
|
||||||
|
|
||||||
class TaggedUsersController;
|
|
||||||
|
|
||||||
class TaggedUsersModel : public SignalVectorModel<TaggedUser>
|
|
||||||
{
|
|
||||||
explicit TaggedUsersModel(QObject *parent);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
// turn a vector item into a model row
|
|
||||||
virtual TaggedUser getItemFromRow(std::vector<QStandardItem *> &row,
|
|
||||||
const TaggedUser &original) override;
|
|
||||||
|
|
||||||
// turns a row in the model into a vector item
|
|
||||||
virtual void getRowFromItem(const TaggedUser &item,
|
|
||||||
std::vector<QStandardItem *> &row) override;
|
|
||||||
|
|
||||||
virtual void afterInit() override;
|
|
||||||
|
|
||||||
// virtual void customRowSetData(const std::vector<QStandardItem *> &row,
|
|
||||||
// int column,
|
|
||||||
// const QVariant &value, int role)
|
|
||||||
// override;
|
|
||||||
|
|
||||||
friend class TaggedUsersController;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace chatterino
|
|
|
@ -156,10 +156,6 @@ void SharedMessageBuilder::parseHighlights()
|
||||||
this->message().flags.set(MessageFlag::Highlighted);
|
this->message().flags.set(MessageFlag::Highlighted);
|
||||||
this->message().highlightColor =
|
this->message().highlightColor =
|
||||||
ColorProvider::instance().color(ColorType::Subscription);
|
ColorProvider::instance().color(ColorType::Subscription);
|
||||||
|
|
||||||
// This message was a subscription.
|
|
||||||
// Don't check for any other highlight phrases.
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Non-common term in SharedMessageBuilder
|
// XXX: Non-common term in SharedMessageBuilder
|
||||||
|
@ -219,7 +215,10 @@ void SharedMessageBuilder::parseHighlights()
|
||||||
<< "sent a message";
|
<< "sent a message";
|
||||||
|
|
||||||
this->message().flags.set(MessageFlag::Highlighted);
|
this->message().flags.set(MessageFlag::Highlighted);
|
||||||
|
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||||
|
{
|
||||||
this->message().highlightColor = userHighlight.getColor();
|
this->message().highlightColor = userHighlight.getColor();
|
||||||
|
}
|
||||||
|
|
||||||
if (userHighlight.showInMentions())
|
if (userHighlight.showInMentions())
|
||||||
{
|
{
|
||||||
|
@ -288,7 +287,10 @@ void SharedMessageBuilder::parseHighlights()
|
||||||
}
|
}
|
||||||
|
|
||||||
this->message().flags.set(MessageFlag::Highlighted);
|
this->message().flags.set(MessageFlag::Highlighted);
|
||||||
|
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||||
|
{
|
||||||
this->message().highlightColor = highlight.getColor();
|
this->message().highlightColor = highlight.getColor();
|
||||||
|
}
|
||||||
|
|
||||||
if (highlight.showInMentions())
|
if (highlight.showInMentions())
|
||||||
{
|
{
|
||||||
|
@ -343,7 +345,11 @@ void SharedMessageBuilder::parseHighlights()
|
||||||
if (!badgeHighlightSet)
|
if (!badgeHighlightSet)
|
||||||
{
|
{
|
||||||
this->message().flags.set(MessageFlag::Highlighted);
|
this->message().flags.set(MessageFlag::Highlighted);
|
||||||
|
if (!this->message().flags.has(MessageFlag::Subscription))
|
||||||
|
{
|
||||||
this->message().highlightColor = highlight.getColor();
|
this->message().highlightColor = highlight.getColor();
|
||||||
|
}
|
||||||
|
|
||||||
badgeHighlightSet = true;
|
badgeHighlightSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const int MAX_FALLOFF_COUNTER = 60;
|
||||||
|
|
||||||
// Ratelimits for joinBucket_
|
// Ratelimits for joinBucket_
|
||||||
const int JOIN_RATELIMIT_BUDGET = 18;
|
const int JOIN_RATELIMIT_BUDGET = 18;
|
||||||
const int JOIN_RATELIMIT_COOLDOWN = 10500;
|
const int JOIN_RATELIMIT_COOLDOWN = 12500;
|
||||||
|
|
||||||
AbstractIrcServer::AbstractIrcServer()
|
AbstractIrcServer::AbstractIrcServer()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#include "IrcChannel2.hpp"
|
#include "IrcChannel2.hpp"
|
||||||
|
|
||||||
#include "debug/AssertInGuiThread.hpp"
|
#include "debug/AssertInGuiThread.hpp"
|
||||||
|
#include "messages/Message.hpp"
|
||||||
#include "messages/MessageBuilder.hpp"
|
#include "messages/MessageBuilder.hpp"
|
||||||
#include "providers/irc/IrcCommands.hpp"
|
#include "providers/irc/IrcCommands.hpp"
|
||||||
#include "providers/irc/IrcServer.hpp"
|
#include "providers/irc/IrcServer.hpp"
|
||||||
|
@ -33,9 +34,14 @@ void IrcChannel::sendMessage(const QString &message)
|
||||||
|
|
||||||
MessageBuilder builder;
|
MessageBuilder builder;
|
||||||
builder.emplace<TimestampElement>();
|
builder.emplace<TimestampElement>();
|
||||||
builder.emplace<TextElement>(this->server()->nick() + ":",
|
const auto &nick = this->server()->nick();
|
||||||
MessageElementFlag::Username);
|
builder.emplace<TextElement>(nick + ":", MessageElementFlag::Username)
|
||||||
|
->setLink({Link::UserInfo, nick});
|
||||||
builder.emplace<TextElement>(message, MessageElementFlag::Text);
|
builder.emplace<TextElement>(message, MessageElementFlag::Text);
|
||||||
|
builder.message().messageText = message;
|
||||||
|
builder.message().searchText = nick + ": " + message;
|
||||||
|
builder.message().loginName = nick;
|
||||||
|
builder.message().displayName = nick;
|
||||||
this->addMessage(builder.release());
|
this->addMessage(builder.release());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include "singletons/Settings.hpp"
|
#include "singletons/Settings.hpp"
|
||||||
#include "singletons/Theme.hpp"
|
#include "singletons/Theme.hpp"
|
||||||
#include "singletons/WindowManager.hpp"
|
#include "singletons/WindowManager.hpp"
|
||||||
|
#include "util/Helpers.hpp"
|
||||||
#include "util/IrcHelpers.hpp"
|
#include "util/IrcHelpers.hpp"
|
||||||
#include "widgets/Window.hpp"
|
#include "widgets/Window.hpp"
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ MessagePtr IrcMessageBuilder::build()
|
||||||
{
|
{
|
||||||
// PARSE
|
// PARSE
|
||||||
this->parse();
|
this->parse();
|
||||||
|
this->usernameColor_ = getRandomColor(this->ircMessage->nick());
|
||||||
|
|
||||||
// PUSH ELEMENTS
|
// PUSH ELEMENTS
|
||||||
this->appendChannelName();
|
this->appendChannelName();
|
||||||
|
|
|
@ -112,11 +112,10 @@ float IrcMessageHandler::similarity(
|
||||||
MessagePtr msg, const LimitedQueueSnapshot<MessagePtr> &messages)
|
MessagePtr msg, const LimitedQueueSnapshot<MessagePtr> &messages)
|
||||||
{
|
{
|
||||||
float similarityPercent = 0.0f;
|
float similarityPercent = 0.0f;
|
||||||
int bySameUser = 0;
|
int checked = 0;
|
||||||
for (int i = 1; bySameUser < getSettings()->hideSimilarMaxMessagesToCheck;
|
for (int i = 1; i <= messages.size(); ++i)
|
||||||
++i)
|
|
||||||
{
|
{
|
||||||
if (messages.size() < i)
|
if (checked >= getSettings()->hideSimilarMaxMessagesToCheck)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -126,11 +125,12 @@ float IrcMessageHandler::similarity(
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (msg->loginName != prevMsg->loginName)
|
if (getSettings()->hideSimilarBySameUser &&
|
||||||
|
msg->loginName != prevMsg->loginName)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
++bySameUser;
|
++checked;
|
||||||
similarityPercent = std::max(
|
similarityPercent = std::max(
|
||||||
similarityPercent,
|
similarityPercent,
|
||||||
relativeSimilarity(msg->messageText, prevMsg->messageText));
|
relativeSimilarity(msg->messageText, prevMsg->messageText));
|
||||||
|
@ -313,13 +313,10 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
|
||||||
const auto highlighted = msg->flags.has(MessageFlag::Highlighted);
|
const auto highlighted = msg->flags.has(MessageFlag::Highlighted);
|
||||||
const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions);
|
const auto showInMentions = msg->flags.has(MessageFlag::ShowInMentions);
|
||||||
|
|
||||||
if (!isSub)
|
|
||||||
{
|
|
||||||
if (highlighted && showInMentions)
|
if (highlighted && showInMentions)
|
||||||
{
|
{
|
||||||
server.mentionsChannel->addMessage(msg);
|
server.mentionsChannel->addMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
chan->addMessage(msg);
|
chan->addMessage(msg);
|
||||||
if (auto chatters = dynamic_cast<ChannelChatters *>(chan.get()))
|
if (auto chatters = dynamic_cast<ChannelChatters *>(chan.get()))
|
||||||
|
@ -813,8 +810,7 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
|
||||||
if (tags == "bad_delete_message_error" || tags == "usage_delete")
|
if (tags == "bad_delete_message_error" || tags == "usage_delete")
|
||||||
{
|
{
|
||||||
channel->addMessage(makeSystemMessage(
|
channel->addMessage(makeSystemMessage(
|
||||||
"Usage: \"/delete <msg-id>\" - can't take more "
|
"Usage: /delete <msg-id>. Can't take more than one argument"));
|
||||||
"than one argument"));
|
|
||||||
}
|
}
|
||||||
else if (tags == "host_on" || tags == "host_target_went_offline")
|
else if (tags == "host_on" || tags == "host_target_went_offline")
|
||||||
{
|
{
|
||||||
|
|
|
@ -377,6 +377,16 @@ void TwitchChannel::sendMessage(const QString &message)
|
||||||
if (parsedMessage == this->lastSentMessage_)
|
if (parsedMessage == this->lastSentMessage_)
|
||||||
{
|
{
|
||||||
auto spaceIndex = parsedMessage.indexOf(' ');
|
auto spaceIndex = parsedMessage.indexOf(' ');
|
||||||
|
// If the message starts with either '/' or '.' Twitch will treat it as a command, omitting
|
||||||
|
// first space and only rest of the arguments treated as actual message content
|
||||||
|
// In cases when user sends a message like ". .a b" first character and first space are omitted as well
|
||||||
|
bool ignoreFirstSpace =
|
||||||
|
parsedMessage.at(0) == '/' || parsedMessage.at(0) == '.';
|
||||||
|
if (ignoreFirstSpace)
|
||||||
|
{
|
||||||
|
spaceIndex = parsedMessage.indexOf(' ', spaceIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (spaceIndex == -1)
|
if (spaceIndex == -1)
|
||||||
{
|
{
|
||||||
// no spaces found, fall back to old magic character
|
// no spaces found, fall back to old magic character
|
||||||
|
|
|
@ -183,29 +183,7 @@ MessagePtr TwitchMessageBuilder::build()
|
||||||
this->emplace<TimestampElement>(
|
this->emplace<TimestampElement>(
|
||||||
calculateMessageTimestamp(this->ircMessage));
|
calculateMessageTimestamp(this->ircMessage));
|
||||||
|
|
||||||
bool addModerationElement = true;
|
if (this->shouldAddModerationElements())
|
||||||
if (this->senderIsBroadcaster)
|
|
||||||
{
|
|
||||||
addModerationElement = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bool hasUserType = this->tags.contains("user-type");
|
|
||||||
if (hasUserType)
|
|
||||||
{
|
|
||||||
QString userType = this->tags.value("user-type").toString();
|
|
||||||
|
|
||||||
if (userType == "mod")
|
|
||||||
{
|
|
||||||
if (!args.isStaffOrBroadcaster)
|
|
||||||
{
|
|
||||||
addModerationElement = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addModerationElement)
|
|
||||||
{
|
{
|
||||||
this->emplace<TwitchModerationElement>();
|
this->emplace<TwitchModerationElement>();
|
||||||
}
|
}
|
||||||
|
@ -660,13 +638,11 @@ void TwitchMessageBuilder::appendUsername()
|
||||||
}
|
}
|
||||||
|
|
||||||
auto nicknames = getCSettings().nicknames.readOnly();
|
auto nicknames = getCSettings().nicknames.readOnly();
|
||||||
auto loginLower = this->message().loginName.toLower();
|
|
||||||
|
|
||||||
for (const auto &nickname : *nicknames)
|
for (const auto &nickname : *nicknames)
|
||||||
{
|
{
|
||||||
if (nickname.name().toLower() == loginLower)
|
if (nickname.match(usernameText))
|
||||||
{
|
{
|
||||||
usernameText = nickname.replace();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1230,6 +1206,24 @@ Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
|
||||||
return Success;
|
return Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TwitchMessageBuilder::shouldAddModerationElements() const
|
||||||
|
{
|
||||||
|
if (this->senderIsBroadcaster)
|
||||||
|
{
|
||||||
|
// You cannot timeout the broadcaster
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->tags.value("user-type").toString() == "mod" &&
|
||||||
|
!this->args.isStaffOrBroadcaster)
|
||||||
|
{
|
||||||
|
// You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void TwitchMessageBuilder::appendChannelPointRewardMessage(
|
void TwitchMessageBuilder::appendChannelPointRewardMessage(
|
||||||
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
|
const ChannelPointReward &reward, MessageBuilder *builder, bool isMod,
|
||||||
bool isBroadcaster)
|
bool isBroadcaster)
|
||||||
|
|
|
@ -90,6 +90,8 @@ private:
|
||||||
void appendFfzBadges();
|
void appendFfzBadges();
|
||||||
Outcome tryParseCheermote(const QString &string);
|
Outcome tryParseCheermote(const QString &string);
|
||||||
|
|
||||||
|
bool shouldAddModerationElements() const;
|
||||||
|
|
||||||
QString roomID_;
|
QString roomID_;
|
||||||
bool hasBits_ = false;
|
bool hasBits_ = false;
|
||||||
QString bits;
|
QString bits;
|
||||||
|
|
|
@ -22,7 +22,6 @@ namespace chatterino {
|
||||||
class HighlightPhrase;
|
class HighlightPhrase;
|
||||||
class HighlightBlacklistUser;
|
class HighlightBlacklistUser;
|
||||||
class IgnorePhrase;
|
class IgnorePhrase;
|
||||||
class TaggedUser;
|
|
||||||
class FilterRecord;
|
class FilterRecord;
|
||||||
class Nickname;
|
class Nickname;
|
||||||
|
|
||||||
|
@ -40,7 +39,6 @@ public:
|
||||||
SignalVector<QString> &mutedChannels;
|
SignalVector<QString> &mutedChannels;
|
||||||
SignalVector<FilterRecordPtr> &filterRecords;
|
SignalVector<FilterRecordPtr> &filterRecords;
|
||||||
SignalVector<Nickname> &nicknames;
|
SignalVector<Nickname> &nicknames;
|
||||||
//SignalVector<TaggedUser> &taggedUsers;
|
|
||||||
SignalVector<ModerationAction> &moderationActions;
|
SignalVector<ModerationAction> &moderationActions;
|
||||||
|
|
||||||
bool isHighlightedUser(const QString &username);
|
bool isHighlightedUser(const QString &username);
|
||||||
|
@ -392,6 +390,8 @@ public:
|
||||||
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
|
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
|
||||||
true};
|
true};
|
||||||
BoolSetting hideSimilar = {"/similarity/hideSimilar", false};
|
BoolSetting hideSimilar = {"/similarity/hideSimilar", false};
|
||||||
|
BoolSetting hideSimilarBySameUser = {"/similarity/hideSimilarBySameUser",
|
||||||
|
true};
|
||||||
BoolSetting hideSimilarMyself = {"/similarity/hideSimilarMyself", false};
|
BoolSetting hideSimilarMyself = {"/similarity/hideSimilarMyself", false};
|
||||||
BoolSetting shownSimilarTriggerHighlights = {
|
BoolSetting shownSimilarTriggerHighlights = {
|
||||||
"/similarity/shownSimilarTriggerHighlights", false};
|
"/similarity/shownSimilarTriggerHighlights", false};
|
||||||
|
|
|
@ -56,11 +56,6 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void actuallyUpdate(double hue, double multiplier) override;
|
void actuallyUpdate(double hue, double multiplier) override;
|
||||||
void fillLookupTableValues(double (&array)[360], double from, double to,
|
|
||||||
double fromValue, double toValue);
|
|
||||||
|
|
||||||
double middleLookupTable_[360] = {};
|
|
||||||
double minLookupTable_[360] = {};
|
|
||||||
|
|
||||||
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;
|
pajlada::Signals::NoArgSignal repaintVisibleChatWidgets_;
|
||||||
|
|
||||||
|
|
|
@ -232,7 +232,9 @@ void Updates::installUpdates()
|
||||||
|
|
||||||
void Updates::checkForUpdates()
|
void Updates::checkForUpdates()
|
||||||
{
|
{
|
||||||
if (!Version::instance().isSupportedOS())
|
auto version = Version::instance();
|
||||||
|
|
||||||
|
if (!version.isSupportedOS())
|
||||||
{
|
{
|
||||||
qCDebug(chatterinoUpdate)
|
qCDebug(chatterinoUpdate)
|
||||||
<< "Update checking disabled because OS doesn't appear to be one "
|
<< "Update checking disabled because OS doesn't appear to be one "
|
||||||
|
@ -241,7 +243,7 @@ void Updates::checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable updates on Flatpak
|
// Disable updates on Flatpak
|
||||||
if (QFileInfo::exists("/.flatpak-info"))
|
if (version.isFlatpak())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -378,20 +378,20 @@ void WindowManager::save()
|
||||||
QJsonDocument document;
|
QJsonDocument document;
|
||||||
|
|
||||||
// "serialize"
|
// "serialize"
|
||||||
QJsonArray window_arr;
|
QJsonArray windowArr;
|
||||||
for (Window *window : this->windows_)
|
for (Window *window : this->windows_)
|
||||||
{
|
{
|
||||||
QJsonObject window_obj;
|
QJsonObject windowObj;
|
||||||
|
|
||||||
// window type
|
// window type
|
||||||
switch (window->getType())
|
switch (window->getType())
|
||||||
{
|
{
|
||||||
case WindowType::Main:
|
case WindowType::Main:
|
||||||
window_obj.insert("type", "main");
|
windowObj.insert("type", "main");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WindowType::Popup:
|
case WindowType::Popup:
|
||||||
window_obj.insert("type", "popup");
|
windowObj.insert("type", "popup");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case WindowType::Attached:;
|
case WindowType::Attached:;
|
||||||
|
@ -399,68 +399,48 @@ void WindowManager::save()
|
||||||
|
|
||||||
if (window->isMaximized())
|
if (window->isMaximized())
|
||||||
{
|
{
|
||||||
window_obj.insert("state", "maximized");
|
windowObj.insert("state", "maximized");
|
||||||
}
|
}
|
||||||
else if (window->isMinimized())
|
else if (window->isMinimized())
|
||||||
{
|
{
|
||||||
window_obj.insert("state", "minimized");
|
windowObj.insert("state", "minimized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// window geometry
|
// window geometry
|
||||||
auto rect = window->getBounds();
|
auto rect = window->getBounds();
|
||||||
|
|
||||||
window_obj.insert("x", rect.x());
|
windowObj.insert("x", rect.x());
|
||||||
window_obj.insert("y", rect.y());
|
windowObj.insert("y", rect.y());
|
||||||
window_obj.insert("width", rect.width());
|
windowObj.insert("width", rect.width());
|
||||||
window_obj.insert("height", rect.height());
|
windowObj.insert("height", rect.height());
|
||||||
|
|
||||||
QJsonObject emote_popup_obj;
|
QJsonObject emotePopupObj;
|
||||||
emote_popup_obj.insert("x", this->emotePopupPos_.x());
|
emotePopupObj.insert("x", this->emotePopupPos_.x());
|
||||||
emote_popup_obj.insert("y", this->emotePopupPos_.y());
|
emotePopupObj.insert("y", this->emotePopupPos_.y());
|
||||||
window_obj.insert("emotePopup", emote_popup_obj);
|
windowObj.insert("emotePopup", emotePopupObj);
|
||||||
|
|
||||||
// window tabs
|
// window tabs
|
||||||
QJsonArray tabs_arr;
|
QJsonArray tabsArr;
|
||||||
|
|
||||||
for (int tab_i = 0; tab_i < window->getNotebook().getPageCount();
|
for (int tabIndex = 0; tabIndex < window->getNotebook().getPageCount();
|
||||||
tab_i++)
|
tabIndex++)
|
||||||
{
|
{
|
||||||
QJsonObject tab_obj;
|
QJsonObject tabObj;
|
||||||
SplitContainer *tab = dynamic_cast<SplitContainer *>(
|
SplitContainer *tab = dynamic_cast<SplitContainer *>(
|
||||||
window->getNotebook().getPageAt(tab_i));
|
window->getNotebook().getPageAt(tabIndex));
|
||||||
assert(tab != nullptr);
|
assert(tab != nullptr);
|
||||||
|
|
||||||
// custom tab title
|
bool isSelected = window->getNotebook().getSelectedPage() == tab;
|
||||||
if (tab->getTab()->hasCustomTitle())
|
WindowManager::encodeTab(tab, isSelected, tabObj);
|
||||||
{
|
tabsArr.append(tabObj);
|
||||||
tab_obj.insert("title", tab->getTab()->getCustomTitle());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// selected
|
windowObj.insert("tabs", tabsArr);
|
||||||
if (window->getNotebook().getSelectedPage() == tab)
|
windowArr.append(windowObj);
|
||||||
{
|
|
||||||
tab_obj.insert("selected", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// highlighting on new messages
|
|
||||||
tab_obj.insert("highlightsEnabled",
|
|
||||||
tab->getTab()->hasHighlightsEnabled());
|
|
||||||
|
|
||||||
// splits
|
|
||||||
QJsonObject splits;
|
|
||||||
|
|
||||||
this->encodeNodeRecursively(tab->getBaseNode(), splits);
|
|
||||||
|
|
||||||
tab_obj.insert("splits2", splits);
|
|
||||||
tabs_arr.append(tab_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
window_obj.insert("tabs", tabs_arr);
|
|
||||||
window_arr.append(window_obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj.insert("windows", window_arr);
|
obj.insert("windows", windowArr);
|
||||||
document.setObject(obj);
|
document.setObject(obj);
|
||||||
|
|
||||||
// save file
|
// save file
|
||||||
|
@ -496,6 +476,32 @@ void WindowManager::queueSave()
|
||||||
this->saveTimer->start(10s);
|
this->saveTimer->start(10s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WindowManager::encodeTab(SplitContainer *tab, bool isSelected,
|
||||||
|
QJsonObject &obj)
|
||||||
|
{
|
||||||
|
// custom tab title
|
||||||
|
if (tab->getTab()->hasCustomTitle())
|
||||||
|
{
|
||||||
|
obj.insert("title", tab->getTab()->getCustomTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
// selected
|
||||||
|
if (isSelected)
|
||||||
|
{
|
||||||
|
obj.insert("selected", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlighting on new messages
|
||||||
|
obj.insert("highlightsEnabled", tab->getTab()->hasHighlightsEnabled());
|
||||||
|
|
||||||
|
// splits
|
||||||
|
QJsonObject splits;
|
||||||
|
|
||||||
|
WindowManager::encodeNodeRecursively(tab->getBaseNode(), splits);
|
||||||
|
|
||||||
|
obj.insert("splits2", splits);
|
||||||
|
}
|
||||||
|
|
||||||
void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
|
void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
|
||||||
{
|
{
|
||||||
switch (node->getType())
|
switch (node->getType())
|
||||||
|
@ -505,11 +511,12 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
|
||||||
obj.insert("moderationMode", node->getSplit()->getModerationMode());
|
obj.insert("moderationMode", node->getSplit()->getModerationMode());
|
||||||
|
|
||||||
QJsonObject split;
|
QJsonObject split;
|
||||||
encodeChannel(node->getSplit()->getIndirectChannel(), split);
|
WindowManager::encodeChannel(node->getSplit()->getIndirectChannel(),
|
||||||
|
split);
|
||||||
obj.insert("data", split);
|
obj.insert("data", split);
|
||||||
|
|
||||||
QJsonArray filters;
|
QJsonArray filters;
|
||||||
encodeFilters(node->getSplit(), filters);
|
WindowManager::encodeFilters(node->getSplit(), filters);
|
||||||
obj.insert("filters", filters);
|
obj.insert("filters", filters);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -519,14 +526,14 @@ void WindowManager::encodeNodeRecursively(SplitNode *node, QJsonObject &obj)
|
||||||
? "horizontal"
|
? "horizontal"
|
||||||
: "vertical");
|
: "vertical");
|
||||||
|
|
||||||
QJsonArray items_arr;
|
QJsonArray itemsArr;
|
||||||
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
|
for (const std::unique_ptr<SplitNode> &n : node->getChildren())
|
||||||
{
|
{
|
||||||
QJsonObject subObj;
|
QJsonObject subObj;
|
||||||
this->encodeNodeRecursively(n.get(), subObj);
|
WindowManager::encodeNodeRecursively(n.get(), subObj);
|
||||||
items_arr.append(subObj);
|
itemsArr.append(subObj);
|
||||||
}
|
}
|
||||||
obj.insert("items", items_arr);
|
obj.insert("items", itemsArr);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ public:
|
||||||
WindowManager();
|
WindowManager();
|
||||||
~WindowManager() override;
|
~WindowManager() override;
|
||||||
|
|
||||||
|
static void encodeTab(SplitContainer *tab, bool isSelected,
|
||||||
|
QJsonObject &obj);
|
||||||
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
static void encodeChannel(IndirectChannel channel, QJsonObject &obj);
|
||||||
static void encodeFilters(Split *split, QJsonArray &arr);
|
static void encodeFilters(Split *split, QJsonArray &arr);
|
||||||
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
|
static IndirectChannel decodeChannel(const SplitDescriptor &descriptor);
|
||||||
|
@ -99,7 +101,8 @@ public:
|
||||||
pajlada::Signals::Signal<SplitContainer *> selectSplitContainer;
|
pajlada::Signals::Signal<SplitContainer *> selectSplitContainer;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void encodeNodeRecursively(SplitContainer::Node *node, QJsonObject &obj);
|
static void encodeNodeRecursively(SplitContainer::Node *node,
|
||||||
|
QJsonObject &obj);
|
||||||
|
|
||||||
// Load window layout from the window-layout.json file
|
// Load window layout from the window-layout.json file
|
||||||
WindowLayout loadWindowLayoutFromFile() const;
|
WindowLayout loadWindowLayoutFromFile() const;
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
|
#include "common/Version.hpp"
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
@ -35,18 +36,6 @@ namespace {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QString getStreamlinkProgram()
|
|
||||||
{
|
|
||||||
if (getSettings()->streamlinkUseCustomPath)
|
|
||||||
{
|
|
||||||
return getSettings()->streamlinkPath + "/" + getBinaryName();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return getBinaryName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool checkStreamlinkPath(const QString &path)
|
bool checkStreamlinkPath(const QString &path)
|
||||||
{
|
{
|
||||||
QFileInfo fileinfo(path);
|
QFileInfo fileinfo(path);
|
||||||
|
@ -83,7 +72,27 @@ namespace {
|
||||||
QProcess *createStreamlinkProcess()
|
QProcess *createStreamlinkProcess()
|
||||||
{
|
{
|
||||||
auto p = new QProcess;
|
auto p = new QProcess;
|
||||||
p->setProgram(getStreamlinkProgram());
|
|
||||||
|
const QString path = [] {
|
||||||
|
if (getSettings()->streamlinkUseCustomPath)
|
||||||
|
{
|
||||||
|
return getSettings()->streamlinkPath + "/" + getBinaryName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return QString{getBinaryName()};
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
if (Version::instance().isFlatpak())
|
||||||
|
{
|
||||||
|
p->setProgram("flatpak-spawn");
|
||||||
|
p->setArguments({"--host", path});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
p->setProgram(path);
|
||||||
|
}
|
||||||
|
|
||||||
QObject::connect(p, &QProcess::errorOccurred, [=](auto err) {
|
QObject::connect(p, &QProcess::errorOccurred, [=](auto err) {
|
||||||
if (err == QProcess::FailedToStart)
|
if (err == QProcess::FailedToStart)
|
||||||
|
@ -165,7 +174,8 @@ void getStreamQualities(const QString &channelURL,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
p->setArguments({channelURL, "--default-stream=KKona"});
|
p->setArguments(p->arguments() +
|
||||||
|
QStringList{channelURL, "--default-stream=KKona"});
|
||||||
|
|
||||||
p->start();
|
p->start();
|
||||||
}
|
}
|
||||||
|
@ -173,7 +183,9 @@ void getStreamQualities(const QString &channelURL,
|
||||||
void openStreamlink(const QString &channelURL, const QString &quality,
|
void openStreamlink(const QString &channelURL, const QString &quality,
|
||||||
QStringList extraArguments)
|
QStringList extraArguments)
|
||||||
{
|
{
|
||||||
QStringList arguments = extraArguments << channelURL << quality;
|
auto proc = createStreamlinkProcess();
|
||||||
|
auto arguments = proc->arguments()
|
||||||
|
<< extraArguments << channelURL << quality;
|
||||||
|
|
||||||
// Remove empty arguments before appending additional streamlink options
|
// Remove empty arguments before appending additional streamlink options
|
||||||
// as the options might purposely contain empty arguments
|
// as the options might purposely contain empty arguments
|
||||||
|
@ -182,7 +194,8 @@ void openStreamlink(const QString &channelURL, const QString &quality,
|
||||||
QString additionalOptions = getSettings()->streamlinkOpts.getValue();
|
QString additionalOptions = getSettings()->streamlinkOpts.getValue();
|
||||||
arguments << splitCommand(additionalOptions);
|
arguments << splitCommand(additionalOptions);
|
||||||
|
|
||||||
bool res = QProcess::startDetached(getStreamlinkProgram(), arguments);
|
proc->setArguments(std::move(arguments));
|
||||||
|
bool res = proc->startDetached();
|
||||||
|
|
||||||
if (!res)
|
if (!res)
|
||||||
{
|
{
|
||||||
|
@ -200,7 +213,7 @@ void openStreamlinkForChannel(const QString &channel)
|
||||||
if (preferredQuality == "choose")
|
if (preferredQuality == "choose")
|
||||||
{
|
{
|
||||||
getStreamQualities(channelURL, [=](QStringList qualityOptions) {
|
getStreamQualities(channelURL, [=](QStringList qualityOptions) {
|
||||||
QualityPopup::showDialog(channel, qualityOptions);
|
QualityPopup::showDialog(channelURL, qualityOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
#include "widgets/BasePopup.hpp"
|
#include "widgets/BasePopup.hpp"
|
||||||
|
|
||||||
|
#include <QAbstractButton>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -20,4 +22,66 @@ void BasePopup::keyPressEvent(QKeyEvent *e)
|
||||||
BaseWindow::keyPressEvent(e);
|
BaseWindow::keyPressEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BasePopup::handleEscape(QKeyEvent *e, QDialogButtonBox *buttonBox)
|
||||||
|
{
|
||||||
|
assert(buttonBox != nullptr);
|
||||||
|
|
||||||
|
if (e->key() == Qt::Key_Escape)
|
||||||
|
{
|
||||||
|
auto buttons = buttonBox->buttons();
|
||||||
|
for (auto *button : buttons)
|
||||||
|
{
|
||||||
|
if (auto role = buttonBox->buttonRole(button);
|
||||||
|
role == QDialogButtonBox::ButtonRole::RejectRole)
|
||||||
|
{
|
||||||
|
button->click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BasePopup::handleEnter(QKeyEvent *e, QDialogButtonBox *buttonBox)
|
||||||
|
{
|
||||||
|
assert(buttonBox != nullptr);
|
||||||
|
|
||||||
|
if (!e->modifiers() ||
|
||||||
|
(e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter))
|
||||||
|
{
|
||||||
|
switch (e->key())
|
||||||
|
{
|
||||||
|
case Qt::Key_Enter:
|
||||||
|
case Qt::Key_Return: {
|
||||||
|
auto buttons = buttonBox->buttons();
|
||||||
|
QAbstractButton *acceptButton = nullptr;
|
||||||
|
for (auto *button : buttons)
|
||||||
|
{
|
||||||
|
if (button->hasFocus())
|
||||||
|
{
|
||||||
|
button->click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto role = buttonBox->buttonRole(button);
|
||||||
|
role == QDialogButtonBox::ButtonRole::AcceptRole)
|
||||||
|
{
|
||||||
|
acceptButton = button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptButton != nullptr)
|
||||||
|
{
|
||||||
|
acceptButton->click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include "common/FlagsEnum.hpp"
|
#include "common/FlagsEnum.hpp"
|
||||||
#include "widgets/BaseWindow.hpp"
|
#include "widgets/BaseWindow.hpp"
|
||||||
|
|
||||||
|
class QDialogButtonBox;
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
class BasePopup : public BaseWindow
|
class BasePopup : public BaseWindow
|
||||||
|
@ -13,6 +15,12 @@ public:
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void keyPressEvent(QKeyEvent *e) override;
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
|
// handleEscape is a helper function for clicking the "Reject" role button of a button box when the Escape button is pressed
|
||||||
|
bool handleEscape(QKeyEvent *e, QDialogButtonBox *buttonBox);
|
||||||
|
|
||||||
|
// handleEnter is a helper function for clicking the "Accept" role button of a button box when Return or Enter is pressed
|
||||||
|
bool handleEnter(QKeyEvent *e, QDialogButtonBox *buttonBox);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -338,6 +338,14 @@ void Window::addShortcuts()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createWindowShortcut(this, "CTRL+SHIFT+N", [this] {
|
||||||
|
if (auto page = dynamic_cast<SplitContainer *>(
|
||||||
|
this->notebook_->getSelectedPage()))
|
||||||
|
{
|
||||||
|
page->popup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom in
|
// Zoom in
|
||||||
{
|
{
|
||||||
auto s = new QShortcut(QKeySequence::ZoomIn, this);
|
auto s = new QShortcut(QKeySequence::ZoomIn, this);
|
||||||
|
|
|
@ -7,35 +7,32 @@
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
QualityPopup::QualityPopup(const QString &_channelName, QStringList options)
|
QualityPopup::QualityPopup(const QString &channelURL, QStringList options)
|
||||||
: BasePopup({},
|
: BasePopup({},
|
||||||
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())))
|
static_cast<QWidget *>(&(getApp()->windows->getMainWindow())))
|
||||||
, channelName_(_channelName)
|
, channelURL_(channelURL)
|
||||||
{
|
{
|
||||||
this->ui_.okButton.setText("OK");
|
this->ui_.selector = new QComboBox(this);
|
||||||
this->ui_.cancelButton.setText("Cancel");
|
this->ui_.vbox = new QVBoxLayout(this);
|
||||||
|
this->ui_.buttonBox = new QDialogButtonBox(
|
||||||
|
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||||
|
|
||||||
QObject::connect(&this->ui_.okButton, &QPushButton::clicked, this,
|
QObject::connect(this->ui_.buttonBox, &QDialogButtonBox::accepted, this,
|
||||||
&QualityPopup::okButtonClicked);
|
&QualityPopup::okButtonClicked);
|
||||||
QObject::connect(&this->ui_.cancelButton, &QPushButton::clicked, this,
|
QObject::connect(this->ui_.buttonBox, &QDialogButtonBox::rejected, this,
|
||||||
&QualityPopup::cancelButtonClicked);
|
&QualityPopup::cancelButtonClicked);
|
||||||
|
|
||||||
this->ui_.buttonBox.addButton(&this->ui_.okButton,
|
this->ui_.selector->addItems(options);
|
||||||
QDialogButtonBox::ButtonRole::AcceptRole);
|
|
||||||
this->ui_.buttonBox.addButton(&this->ui_.cancelButton,
|
|
||||||
QDialogButtonBox::ButtonRole::RejectRole);
|
|
||||||
|
|
||||||
this->ui_.selector.addItems(options);
|
this->ui_.vbox->addWidget(this->ui_.selector);
|
||||||
|
this->ui_.vbox->addWidget(this->ui_.buttonBox);
|
||||||
|
|
||||||
this->ui_.vbox.addWidget(&this->ui_.selector);
|
this->setLayout(this->ui_.vbox);
|
||||||
this->ui_.vbox.addWidget(&this->ui_.buttonBox);
|
|
||||||
|
|
||||||
this->setLayout(&this->ui_.vbox);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QualityPopup::showDialog(const QString &channelName, QStringList options)
|
void QualityPopup::showDialog(const QString &channelURL, QStringList options)
|
||||||
{
|
{
|
||||||
QualityPopup *instance = new QualityPopup(channelName, options);
|
QualityPopup *instance = new QualityPopup(channelURL, options);
|
||||||
|
|
||||||
instance->window()->setWindowTitle("Chatterino - select stream quality");
|
instance->window()->setWindowTitle("Chatterino - select stream quality");
|
||||||
instance->setAttribute(Qt::WA_DeleteOnClose, true);
|
instance->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||||
|
@ -43,16 +40,27 @@ void QualityPopup::showDialog(const QString &channelName, QStringList options)
|
||||||
instance->show();
|
instance->show();
|
||||||
instance->activateWindow();
|
instance->activateWindow();
|
||||||
instance->raise();
|
instance->raise();
|
||||||
instance->setFocus();
|
}
|
||||||
|
|
||||||
|
void QualityPopup::keyPressEvent(QKeyEvent *e)
|
||||||
|
{
|
||||||
|
if (this->handleEscape(e, this->ui_.buttonBox))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->handleEnter(e, this->ui_.buttonBox))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BasePopup::keyPressEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
void QualityPopup::okButtonClicked()
|
void QualityPopup::okButtonClicked()
|
||||||
{
|
{
|
||||||
QString channelURL = "twitch.tv/" + this->channelName_;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
openStreamlink(channelURL, this->ui_.selector.currentText());
|
openStreamlink(this->channelURL_, this->ui_.selector->currentText());
|
||||||
}
|
}
|
||||||
catch (const Exception &ex)
|
catch (const Exception &ex)
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QPushButton>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
@ -12,22 +11,23 @@ namespace chatterino {
|
||||||
class QualityPopup : public BasePopup
|
class QualityPopup : public BasePopup
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
QualityPopup(const QString &_channelName, QStringList options);
|
QualityPopup(const QString &channelURL, QStringList options);
|
||||||
static void showDialog(const QString &_channelName, QStringList options);
|
static void showDialog(const QString &channelURL, QStringList options);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void okButtonClicked();
|
void okButtonClicked();
|
||||||
void cancelButtonClicked();
|
void cancelButtonClicked();
|
||||||
|
|
||||||
struct {
|
struct {
|
||||||
QVBoxLayout vbox;
|
QVBoxLayout *vbox;
|
||||||
QComboBox selector;
|
QComboBox *selector;
|
||||||
QDialogButtonBox buttonBox;
|
QDialogButtonBox *buttonBox;
|
||||||
QPushButton okButton;
|
|
||||||
QPushButton cancelButton;
|
|
||||||
} ui_;
|
} ui_;
|
||||||
|
|
||||||
QString channelName_;
|
QString channelURL_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
const QString TEXT_VIEWS("Views: %1");
|
const QString TEXT_VIEWS("Views: %1");
|
||||||
const QString TEXT_FOLLOWERS("Followers: %1");
|
const QString TEXT_FOLLOWERS("Followers: %1");
|
||||||
const QString TEXT_CREATED("Created: %1");
|
const QString TEXT_CREATED("Created: %1");
|
||||||
const QString TEXT_TITLE("%1's Usercard");
|
const QString TEXT_TITLE("%1's Usercard - #%2");
|
||||||
#define TEXT_USER_ID "ID: "
|
#define TEXT_USER_ID "ID: "
|
||||||
#define TEXT_UNAVAILABLE "(not available)"
|
#define TEXT_UNAVAILABLE "(not available)"
|
||||||
|
|
||||||
|
@ -513,7 +513,7 @@ void UserInfoPopup::setData(const QString &name, const ChannelPtr &channel)
|
||||||
{
|
{
|
||||||
this->userName_ = name;
|
this->userName_ = name;
|
||||||
this->channel_ = channel;
|
this->channel_ = channel;
|
||||||
this->setWindowTitle(TEXT_TITLE.arg(name));
|
this->setWindowTitle(TEXT_TITLE.arg(name, channel->getName()));
|
||||||
|
|
||||||
this->ui_.nameLabel->setText(name);
|
this->ui_.nameLabel->setText(name);
|
||||||
this->ui_.nameLabel->setProperty("copy-text", name);
|
this->ui_.nameLabel->setProperty("copy-text", name);
|
||||||
|
@ -598,7 +598,8 @@ void UserInfoPopup::updateUserData()
|
||||||
this->avatarUrl_ = user.profileImageUrl;
|
this->avatarUrl_ = user.profileImageUrl;
|
||||||
|
|
||||||
this->ui_.nameLabel->setText(user.displayName);
|
this->ui_.nameLabel->setText(user.displayName);
|
||||||
this->setWindowTitle(TEXT_TITLE.arg(user.displayName));
|
this->setWindowTitle(
|
||||||
|
TEXT_TITLE.arg(user.displayName, this->channel_->getName()));
|
||||||
this->ui_.viewCountLabel->setText(
|
this->ui_.viewCountLabel->setText(
|
||||||
TEXT_VIEWS.arg(localizeNumbers(user.viewCount)));
|
TEXT_VIEWS.arg(localizeNumbers(user.viewCount)));
|
||||||
this->ui_.createdDateLabel->setText(
|
this->ui_.createdDateLabel->setText(
|
||||||
|
|
|
@ -2101,12 +2101,24 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
value.replace("{user}", layout->getMessage()->loginName)
|
value = getApp()->commands->execCustomCommand(
|
||||||
.replace("{channel}", this->channel_->getName())
|
QStringList(), Command{"(modaction)", value}, true, channel,
|
||||||
.replace("{msg-id}", layout->getMessage()->id)
|
{
|
||||||
.replace("{message}", layout->getMessage()->messageText);
|
{"user.name", layout->getMessage()->loginName},
|
||||||
|
{"msg.id", layout->getMessage()->id},
|
||||||
|
{"msg.text", layout->getMessage()->messageText},
|
||||||
|
|
||||||
|
// old placeholders
|
||||||
|
{"user", layout->getMessage()->loginName},
|
||||||
|
{"msg-id", layout->getMessage()->id},
|
||||||
|
{"message", layout->getMessage()->messageText},
|
||||||
|
|
||||||
|
// new version of this is inside execCustomCommand
|
||||||
|
{"channel", this->channel()->getName()},
|
||||||
|
});
|
||||||
|
|
||||||
value = getApp()->commands->execCommand(value, channel, false);
|
value = getApp()->commands->execCommand(value, channel, false);
|
||||||
|
|
||||||
channel->sendMessage(value);
|
channel->sendMessage(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -64,6 +64,16 @@ NotebookTab::NotebookTab(Notebook *notebook)
|
||||||
this->notebook_->removePage(this->page);
|
this->notebook_->removePage(this->page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this->menu_.addAction(
|
||||||
|
"Popup Tab",
|
||||||
|
[=]() {
|
||||||
|
if (auto container = dynamic_cast<SplitContainer *>(this->page))
|
||||||
|
{
|
||||||
|
container->popup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QKeySequence("Ctrl+Shift+N"));
|
||||||
|
|
||||||
highlightNewMessagesAction_ =
|
highlightNewMessagesAction_ =
|
||||||
new QAction("Mark Tab as Unread on New Messages", &this->menu_);
|
new QAction("Mark Tab as Unread on New Messages", &this->menu_);
|
||||||
highlightNewMessagesAction_->setCheckable(true);
|
highlightNewMessagesAction_->setCheckable(true);
|
||||||
|
|
|
@ -110,7 +110,6 @@ AboutPage::AboutPage()
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
l.emplace<QLabel>("Chatterino Wiki can be found <a href=\"" LINK_CHATTERINO_WIKI "\">here</a>")->setOpenExternalLinks(true);
|
l.emplace<QLabel>("Chatterino Wiki can be found <a href=\"" LINK_CHATTERINO_WIKI "\">here</a>")->setOpenExternalLinks(true);
|
||||||
l.emplace<QLabel>("Support <a href=\"" LINK_DONATE "\">Chatterino</a>")->setOpenExternalLinks(true);
|
|
||||||
l.emplace<QLabel>("All about Chatterino's <a href=\"" LINK_CHATTERINO_FEATURES "\">features</a>")->setOpenExternalLinks(true);
|
l.emplace<QLabel>("All about Chatterino's <a href=\"" LINK_CHATTERINO_FEATURES "\">features</a>")->setOpenExternalLinks(true);
|
||||||
l.emplace<QLabel>("Join the official Chatterino <a href=\"" LINK_CHATTERINO_DISCORD "\">Discord</a>")->setOpenExternalLinks(true);
|
l.emplace<QLabel>("Join the official Chatterino <a href=\"" LINK_CHATTERINO_DISCORD "\">Discord</a>")->setOpenExternalLinks(true);
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
|
@ -520,11 +520,11 @@ void GeneralPage::initLayout(GeneralPageView &layout)
|
||||||
layout.addCheckbox("Title", s.headerStreamTitle);
|
layout.addCheckbox("Title", s.headerStreamTitle);
|
||||||
|
|
||||||
layout.addSubtitle("R9K");
|
layout.addSubtitle("R9K");
|
||||||
layout.addDescription(
|
layout.addDescription("Hide similar messages. Toggle hidden "
|
||||||
"Hide similar messages by the same user. Toggle hidden "
|
|
||||||
"messages by pressing Ctrl+H.");
|
"messages by pressing Ctrl+H.");
|
||||||
layout.addCheckbox("Hide similar messages", s.similarityEnabled);
|
layout.addCheckbox("Hide similar messages", s.similarityEnabled);
|
||||||
//layout.addCheckbox("Gray out matches", s.colorSimilarDisabled);
|
//layout.addCheckbox("Gray out matches", s.colorSimilarDisabled);
|
||||||
|
layout.addCheckbox("By the same user", s.hideSimilarBySameUser);
|
||||||
layout.addCheckbox("Hide my own messages", s.hideSimilarMyself);
|
layout.addCheckbox("Hide my own messages", s.hideSimilarMyself);
|
||||||
layout.addCheckbox("Receive notification sounds from hidden messages",
|
layout.addCheckbox("Receive notification sounds from hidden messages",
|
||||||
s.shownSimilarTriggerHighlights);
|
s.shownSimilarTriggerHighlights);
|
||||||
|
|
|
@ -49,7 +49,7 @@ void addPhrasesTab(LayoutCreator<QVBoxLayout> layout)
|
||||||
->initialized(&getSettings()->ignoredMessages))
|
->initialized(&getSettings()->ignoredMessages))
|
||||||
.getElement();
|
.getElement();
|
||||||
view->setTitles(
|
view->setTitles(
|
||||||
{"Pattern", "Regex", "Case Sensitive", "Block", "Replacement"});
|
{"Pattern", "Regex", "Case-sensitive", "Block", "Replacement"});
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
QHeaderView::Fixed);
|
QHeaderView::Fixed);
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
|
|
@ -44,6 +44,8 @@ KeyboardSettingsPage::KeyboardSettingsPage()
|
||||||
form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab"));
|
form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab"));
|
||||||
form->addRow(new QLabel("Ctrl + Shift + W"),
|
form->addRow(new QLabel("Ctrl + Shift + W"),
|
||||||
new QLabel("Close current tab"));
|
new QLabel("Close current tab"));
|
||||||
|
form->addRow(new QLabel("Ctrl + Shift + N"),
|
||||||
|
new QLabel("Open current tab as a popup"));
|
||||||
form->addRow(new QLabel("Ctrl + H"),
|
form->addRow(new QLabel("Ctrl + H"),
|
||||||
new QLabel("Hide/Show similar messages (See General->R9K)"));
|
new QLabel("Hide/Show similar messages (See General->R9K)"));
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
#include "controllers/moderationactions/ModerationActionModel.hpp"
|
#include "controllers/moderationactions/ModerationActionModel.hpp"
|
||||||
#include "controllers/taggedusers/TaggedUsersModel.hpp"
|
|
||||||
#include "singletons/Logging.hpp"
|
#include "singletons/Logging.hpp"
|
||||||
#include "singletons/Paths.hpp"
|
#include "singletons/Paths.hpp"
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
|
@ -159,8 +158,8 @@ ModerationPage::ModerationPage()
|
||||||
// clang-format off
|
// clang-format off
|
||||||
auto label = modMode.emplace<QLabel>(
|
auto label = modMode.emplace<QLabel>(
|
||||||
"Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>"
|
"Moderation mode is enabled by clicking <img width='18' height='18' src=':/buttons/modModeDisabled.png'> in a channel that you moderate.<br><br>"
|
||||||
"Moderation buttons can be bound to chat commands such as \"/ban {user}\", \"/timeout {user} 1000\", \"/w someusername !report {user} was bad in channel {channel}\" or any other custom text commands.<br>"
|
"Moderation buttons can be bound to chat commands such as \"/ban {user.name}\", \"/timeout {user.name} 1000\", \"/w someusername !report {user.name} was bad in channel {channel.name}\" or any other custom text commands.<br>"
|
||||||
"For deleting messages use /delete {msg-id}.<br><br>"
|
"For deleting messages use /delete {msg.id}.<br><br>"
|
||||||
"More information can be found <a href='https://wiki.chatterino.com/Moderation/#moderation-mode'>here</a>.");
|
"More information can be found <a href='https://wiki.chatterino.com/Moderation/#moderation-mode'>here</a>.");
|
||||||
label->setOpenExternalLinks(true);
|
label->setOpenExternalLinks(true);
|
||||||
label->setWordWrap(true);
|
label->setWordWrap(true);
|
||||||
|
@ -190,22 +189,8 @@ ModerationPage::ModerationPage()
|
||||||
|
|
||||||
view->addButtonPressed.connect([] {
|
view->addButtonPressed.connect([] {
|
||||||
getSettings()->moderationActions.append(
|
getSettings()->moderationActions.append(
|
||||||
ModerationAction("/timeout {user} 300"));
|
ModerationAction("/timeout {user.name} 300"));
|
||||||
});
|
});
|
||||||
|
|
||||||
/*auto taggedUsers = tabs.appendTab(new QVBoxLayout, "Tagged users");
|
|
||||||
{
|
|
||||||
EditableModelView *view = *taggedUsers.emplace<EditableModelView>(
|
|
||||||
app->taggedUsers->createModel(nullptr));
|
|
||||||
|
|
||||||
view->setTitles({"Name"});
|
|
||||||
view->getTableView()->horizontalHeader()->setStretchLastSection(true);
|
|
||||||
|
|
||||||
view->addButtonPressed.connect([] {
|
|
||||||
getApp()->taggedUsers->users.appendItem(
|
|
||||||
TaggedUser(ProviderId::Twitch, "example", "xD"));
|
|
||||||
});
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->addModerationButtonSettings(tabs);
|
this->addModerationButtonSettings(tabs);
|
||||||
|
|
|
@ -29,19 +29,22 @@ NicknamesPage::NicknamesPage()
|
||||||
->initialized(&getSettings()->nicknames))
|
->initialized(&getSettings()->nicknames))
|
||||||
.getElement();
|
.getElement();
|
||||||
|
|
||||||
view->setTitles({"Username", "Nickname"});
|
view->setTitles({"Username", "Nickname", "Enable regex", "Case-sensitive"});
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
QHeaderView::Interactive);
|
QHeaderView::Fixed);
|
||||||
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
|
0, QHeaderView::Stretch);
|
||||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||||
1, QHeaderView::Stretch);
|
1, QHeaderView::Stretch);
|
||||||
|
|
||||||
view->addButtonPressed.connect([] {
|
view->addButtonPressed.connect([] {
|
||||||
getSettings()->nicknames.append(Nickname{"Username", "Nickname"});
|
getSettings()->nicknames.append(
|
||||||
|
Nickname{"Username", "Nickname", false, false});
|
||||||
});
|
});
|
||||||
|
|
||||||
QTimer::singleShot(1, [view] {
|
QTimer::singleShot(1, [view] {
|
||||||
view->getTableView()->resizeColumnsToContents();
|
view->getTableView()->resizeColumnsToContents();
|
||||||
view->getTableView()->setColumnWidth(0, 250);
|
view->getTableView()->setColumnWidth(0, 200);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -262,10 +262,11 @@ Split::Split(QWidget *parent)
|
||||||
if (getSettings()->askOnImageUpload.getValue())
|
if (getSettings()->askOnImageUpload.getValue())
|
||||||
{
|
{
|
||||||
QMessageBox msgBox;
|
QMessageBox msgBox;
|
||||||
|
msgBox.setWindowTitle("Chatterino");
|
||||||
msgBox.setText("Image upload");
|
msgBox.setText("Image upload");
|
||||||
msgBox.setInformativeText(
|
msgBox.setInformativeText(
|
||||||
"You are uploading an image to a 3rd party service not in "
|
"You are uploading an image to a 3rd party service not in "
|
||||||
"control of the chatterino team. You may not be able to "
|
"control of the Chatterino team. You may not be able to "
|
||||||
"remove the image from the site. Are you okay with this?");
|
"remove the image from the site. Are you okay with this?");
|
||||||
msgBox.addButton(QMessageBox::Cancel);
|
msgBox.addButton(QMessageBox::Cancel);
|
||||||
msgBox.addButton(QMessageBox::Yes);
|
msgBox.addButton(QMessageBox::Yes);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include "util/Helpers.hpp"
|
#include "util/Helpers.hpp"
|
||||||
#include "util/LayoutCreator.hpp"
|
#include "util/LayoutCreator.hpp"
|
||||||
#include "widgets/Notebook.hpp"
|
#include "widgets/Notebook.hpp"
|
||||||
|
#include "widgets/Window.hpp"
|
||||||
#include "widgets/helper/ChannelView.hpp"
|
#include "widgets/helper/ChannelView.hpp"
|
||||||
#include "widgets/helper/NotebookTab.hpp"
|
#include "widgets/helper/NotebookTab.hpp"
|
||||||
#include "widgets/splits/ClosedSplits.hpp"
|
#include "widgets/splits/ClosedSplits.hpp"
|
||||||
|
@ -761,6 +762,33 @@ void SplitContainer::applyFromDescriptor(const NodeDescriptor &rootNode)
|
||||||
this->layout();
|
this->layout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SplitContainer::popup()
|
||||||
|
{
|
||||||
|
Window &window = getApp()->windows->createWindow(WindowType::Popup);
|
||||||
|
auto popupContainer = window.getNotebook().getOrAddSelectedPage();
|
||||||
|
|
||||||
|
QJsonObject encodedTab;
|
||||||
|
WindowManager::encodeTab(this, true, encodedTab);
|
||||||
|
TabDescriptor tab = TabDescriptor::loadFromJSON(encodedTab);
|
||||||
|
|
||||||
|
// custom title
|
||||||
|
if (!tab.customTitle_.isEmpty())
|
||||||
|
{
|
||||||
|
popupContainer->getTab()->setCustomTitle(tab.customTitle_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlighting on new messages
|
||||||
|
popupContainer->getTab()->setHighlightsEnabled(tab.highlightsEnabled_);
|
||||||
|
|
||||||
|
// splits
|
||||||
|
if (tab.rootNode_)
|
||||||
|
{
|
||||||
|
popupContainer->applyFromDescriptor(*tab.rootNode_);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.show();
|
||||||
|
}
|
||||||
|
|
||||||
void SplitContainer::applyFromDescriptorRecursively(
|
void SplitContainer::applyFromDescriptorRecursively(
|
||||||
const NodeDescriptor &rootNode, Node *node)
|
const NodeDescriptor &rootNode, Node *node)
|
||||||
{
|
{
|
||||||
|
|
|
@ -202,6 +202,8 @@ public:
|
||||||
|
|
||||||
void applyFromDescriptor(const NodeDescriptor &rootNode);
|
void applyFromDescriptor(const NodeDescriptor &rootNode);
|
||||||
|
|
||||||
|
void popup();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
||||||
|
|
|
@ -563,7 +563,7 @@ void SplitInput::insertCompletionText(const QString &input_)
|
||||||
auto input = input_ + ' ';
|
auto input = input_ + ' ';
|
||||||
|
|
||||||
auto text = edit.toPlainText();
|
auto text = edit.toPlainText();
|
||||||
auto position = edit.textCursor().position();
|
auto position = edit.textCursor().position() - 1;
|
||||||
|
|
||||||
for (int i = clamp(position, 0, static_cast<int>(text.length() - 1));
|
for (int i = clamp(position, 0, static_cast<int>(text.length() - 1));
|
||||||
i >= 0; i--)
|
i >= 0; i--)
|
||||||
|
@ -585,7 +585,7 @@ void SplitInput::insertCompletionText(const QString &input_)
|
||||||
if (done)
|
if (done)
|
||||||
{
|
{
|
||||||
auto cursor = edit.textCursor();
|
auto cursor = edit.textCursor();
|
||||||
edit.setText(text.remove(i, position - i).insert(i, input));
|
edit.setText(text.remove(i, position - i + 1).insert(i, input));
|
||||||
|
|
||||||
cursor.setPosition(i + input.size());
|
cursor.setPosition(i + input.size());
|
||||||
edit.setTextCursor(cursor);
|
edit.setTextCursor(cursor);
|
||||||
|
@ -641,9 +641,6 @@ void SplitInput::editTextChanged()
|
||||||
this->textChanged.invoke(text);
|
this->textChanged.invoke(text);
|
||||||
|
|
||||||
text = text.trimmed();
|
text = text.trimmed();
|
||||||
static QRegularExpression spaceRegex("\\s\\s+");
|
|
||||||
text = text.replace(spaceRegex, " ");
|
|
||||||
|
|
||||||
text =
|
text =
|
||||||
app->commands->execCommand(text, this->split_->getChannel(), true);
|
app->commands->execCommand(text, this->split_->getChannel(), true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ project(chatterino-test)
|
||||||
|
|
||||||
set(test_SOURCES
|
set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/ChannelChatters.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp
|
||||||
|
|
113
tests/src/ChannelChatters.cpp
Normal file
113
tests/src/ChannelChatters.cpp
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#include "common/ChannelChatters.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
class MockChannel : public Channel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockChannel(const QString &name)
|
||||||
|
: Channel(name, Channel::Type::Twitch)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
// Ensure inserting the same user does not increase the size of the stored colors
|
||||||
|
TEST(ChatterChatters, insertSameUser)
|
||||||
|
{
|
||||||
|
MockChannel channel("test");
|
||||||
|
|
||||||
|
ChannelChatters chatters(channel);
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 0);
|
||||||
|
chatters.setUserColor("pajlada", QColor("#fff"));
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 1);
|
||||||
|
chatters.setUserColor("pajlada", QColor("#fff"));
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we can update a chatters color
|
||||||
|
TEST(ChatterChatters, insertSameUserUpdatesColor)
|
||||||
|
{
|
||||||
|
MockChannel channel("test");
|
||||||
|
|
||||||
|
ChannelChatters chatters(channel);
|
||||||
|
|
||||||
|
chatters.setUserColor("pajlada", QColor("#fff"));
|
||||||
|
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#fff"));
|
||||||
|
chatters.setUserColor("pajlada", QColor("#f0f"));
|
||||||
|
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#f0f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure getting a non-existant users color returns an invalid QColor
|
||||||
|
TEST(ChatterChatters, getNonExistantUser)
|
||||||
|
{
|
||||||
|
MockChannel channel("test");
|
||||||
|
|
||||||
|
ChannelChatters chatters(channel);
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.getUserColor("nonexistantuser"), QColor());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure getting a user doesn't create an entry
|
||||||
|
TEST(ChatterChatters, getDoesNotCreate)
|
||||||
|
{
|
||||||
|
MockChannel channel("test");
|
||||||
|
|
||||||
|
ChannelChatters chatters(channel);
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 0);
|
||||||
|
chatters.getUserColor("nonexistantuser");
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the least recently used entry is purged when we reach MAX_SIZE
|
||||||
|
TEST(ChatterChatters, insertMaxSize)
|
||||||
|
{
|
||||||
|
MockChannel channel("test");
|
||||||
|
|
||||||
|
ChannelChatters chatters(channel);
|
||||||
|
|
||||||
|
// Prime chatters with 2 control entries
|
||||||
|
chatters.setUserColor("pajlada", QColor("#f00"));
|
||||||
|
chatters.setUserColor("zneix", QColor("#f0f"));
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor("#f00"));
|
||||||
|
EXPECT_EQ(chatters.getUserColor("zneix"), QColor("#f0f"));
|
||||||
|
EXPECT_EQ(chatters.getUserColor("nonexistantuser"), QColor());
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.colorsSize(), 2);
|
||||||
|
|
||||||
|
for (int i = 0; i < ChannelChatters::maxChatterColorCount - 1; ++i)
|
||||||
|
{
|
||||||
|
auto username = QString("user%1").arg(i);
|
||||||
|
chatters.setUserColor(username, QColor("#00f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have bumped ONE entry out (pajlada)
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor());
|
||||||
|
EXPECT_EQ(chatters.getUserColor("zneix"), QColor("#f0f"));
|
||||||
|
EXPECT_EQ(chatters.getUserColor("user1"), QColor("#00f"));
|
||||||
|
|
||||||
|
chatters.setUserColor("newuser", QColor("#00e"));
|
||||||
|
|
||||||
|
for (int i = 0; i < ChannelChatters::maxChatterColorCount; ++i)
|
||||||
|
{
|
||||||
|
auto username = QString("user%1").arg(i);
|
||||||
|
chatters.setUserColor(username, QColor("#00f"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// One more entry should be bumped out (zneix)
|
||||||
|
|
||||||
|
EXPECT_EQ(chatters.getUserColor("pajlada"), QColor());
|
||||||
|
EXPECT_EQ(chatters.getUserColor("zneix"), QColor());
|
||||||
|
EXPECT_EQ(chatters.getUserColor("user1"), QColor("#00f"));
|
||||||
|
}
|
Loading…
Reference in a new issue