mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-25 06:54:43 +00:00
feat: message filters initial
This commit is contained in:
@@ -91,3 +91,6 @@
|
||||
[submodule "Telegram/ThirdParty/xdg-desktop-portal"]
|
||||
path = Telegram/ThirdParty/xdg-desktop-portal
|
||||
url = https://github.com/flatpak/xdg-desktop-portal.git
|
||||
[submodule "Telegram/lib_icu"]
|
||||
path = Telegram/lib_icu
|
||||
url = https://github.com/AyuGram/lib_icu.git
|
||||
|
||||
@@ -20,6 +20,10 @@ add_subdirectory(lib_webrtc)
|
||||
add_subdirectory(lib_webview)
|
||||
add_subdirectory(codegen)
|
||||
|
||||
# AyuGram filters
|
||||
add_subdirectory(lib_icu)
|
||||
# AyuGram filters
|
||||
|
||||
get_filename_component(src_loc SourceFiles REALPATH)
|
||||
get_filename_component(res_loc Resources REALPATH)
|
||||
|
||||
@@ -92,6 +96,9 @@ PRIVATE
|
||||
desktop-app::external_openssl
|
||||
desktop-app::external_openal
|
||||
desktop-app::external_xxhash
|
||||
|
||||
# AyuGram filters
|
||||
ayugram::lib_icu
|
||||
)
|
||||
|
||||
set(ayugram_files
|
||||
@@ -137,10 +144,18 @@ set(ayugram_files
|
||||
ayu/ui/settings/settings_general.h
|
||||
ayu/ui/settings/settings_ayu.cpp
|
||||
ayu/ui/settings/settings_ayu.h
|
||||
ayu/ui/settings/settings_filters.cpp
|
||||
ayu/ui/settings/settings_filters.h
|
||||
ayu/ui/settings/settings_main.cpp
|
||||
ayu/ui/settings/settings_main.h
|
||||
ayu/ui/settings/settings_other.cpp
|
||||
ayu/ui/settings/settings_other.h
|
||||
ayu/ui/settings/filters/settings_filters_list.cpp
|
||||
ayu/ui/settings/filters/settings_filters_list.h
|
||||
ayu/ui/settings/filters/per_dialog_filter.cpp
|
||||
ayu/ui/settings/filters/per_dialog_filter.h
|
||||
ayu/ui/settings/filters/edit_filter.cpp
|
||||
ayu/ui/settings/filters/edit_filter.h
|
||||
ayu/ui/context_menu/context_menu.cpp
|
||||
ayu/ui/context_menu/context_menu.h
|
||||
ayu/ui/context_menu/menu_item_subtext.cpp
|
||||
@@ -155,6 +170,8 @@ set(ayugram_files
|
||||
ayu/ui/boxes/edit_mark_box.h
|
||||
ayu/ui/boxes/font_selector.cpp
|
||||
ayu/ui/boxes/font_selector.h
|
||||
ayu/ui/boxes/import_filters_box.cpp
|
||||
ayu/ui/boxes/import_filters_box.h
|
||||
ayu/ui/boxes/theme_selector_box.cpp
|
||||
ayu/ui/boxes/theme_selector_box.h
|
||||
ayu/ui/boxes/message_shot_box.cpp
|
||||
@@ -198,6 +215,14 @@ set(ayugram_files
|
||||
ayu/features/translator/implementations/telegram.h
|
||||
ayu/features/translator/implementations/base.cpp
|
||||
ayu/features/translator/implementations/base.h
|
||||
ayu/features/filters/filters_controller.cpp
|
||||
ayu/features/filters/filters_controller.h
|
||||
ayu/features/filters/filters_cache_controller.cpp
|
||||
ayu/features/filters/filters_cache_controller.h
|
||||
ayu/features/filters/filters_utils.cpp
|
||||
ayu/features/filters/filters_utils.h
|
||||
ayu/features/filters/shadow_ban_utils.cpp
|
||||
ayu/features/filters/shadow_ban_utils.h
|
||||
ayu/data/messages_storage.cpp
|
||||
ayu/data/messages_storage.h
|
||||
ayu/data/entities.h
|
||||
@@ -2036,6 +2061,7 @@ PRIVATE
|
||||
TDESKTOP_API_ID=${TDESKTOP_API_ID}
|
||||
TDESKTOP_API_HASH=${TDESKTOP_API_HASH}
|
||||
G_LOG_DOMAIN="Telegram"
|
||||
U_STATIC_IMPLEMENTATION=1 # AyuGram: do not use __declspec(dllimport) when including icu headers
|
||||
)
|
||||
|
||||
get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
|
||||
@@ -6987,9 +6987,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"ayu_ImportDataSuccess" = "Database imported successfully.";
|
||||
"ayu_ImportDataFailure" = "Failed to import database.";
|
||||
"ayu_RegexFilters" = "Message Filters";
|
||||
"ayu_RegexFiltersAmount#one" = "1 filter";
|
||||
"ayu_RegexFiltersAmount#one" = "{count} filter";
|
||||
"ayu_RegexFiltersAmount#other" = "{count} filters";
|
||||
"ayu_RegexFiltersExcludedAmount#one" = "1 excluded";
|
||||
"ayu_RegexFiltersExcludedAmount#one" = "{count} excluded";
|
||||
"ayu_RegexFiltersExcludedAmount#other" = "{count} excluded";
|
||||
"ayu_RegexFiltersHeader" = "Filters";
|
||||
"ayu_RegexFiltersShared" = "Shared Filters";
|
||||
@@ -7029,17 +7029,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
"ayu_FiltersToastFailNoChanges" = "Filters are the same as current.";
|
||||
"ayu_FiltersToastSuccess" = "Filters successfully imported.";
|
||||
"ayu_FiltersSheetTitle" = "Apply filter changes?";
|
||||
"ayu_FiltersSheetNewFilters#one" = "**1** new filter";
|
||||
"ayu_FiltersSheetNewFilters#one" = "**{count}** new filter";
|
||||
"ayu_FiltersSheetNewFilters#other" = "**{count}** new filters";
|
||||
"ayu_FiltersSheetRemovedFilters#one" = "**1** removed filter";
|
||||
"ayu_FiltersSheetRemovedFilters#one" = "**{count}** removed filter";
|
||||
"ayu_FiltersSheetRemovedFilters#other" = "**{count}** removed filters";
|
||||
"ayu_FiltersSheetUpdatedFilters#one" = "**1** updated filter";
|
||||
"ayu_FiltersSheetUpdatedFilters#one" = "**{count}** updated filter";
|
||||
"ayu_FiltersSheetUpdatedFilters#other" = "**{count}** updated filters";
|
||||
"ayu_FiltersSheetNewExclusions#one" = "**1** new exclusion";
|
||||
"ayu_FiltersSheetNewExclusions#one" = "**{count}** new exclusion";
|
||||
"ayu_FiltersSheetNewExclusions#other" = "**{count}** new exclusions";
|
||||
"ayu_FiltersSheetRemovedExclusions#one" = "**1** removed exclusion";
|
||||
"ayu_FiltersSheetRemovedExclusions#one" = "**{count}** removed exclusion";
|
||||
"ayu_FiltersSheetRemovedExclusions#other" = "**{count}** removed exclusions";
|
||||
"ayu_FiltersSheetDialogsToResolve#one" = "**1** dialog to resolve";
|
||||
"ayu_FiltersSheetDialogsToResolve#one" = "**{count}** dialog to resolve";
|
||||
"ayu_FiltersSheetDialogsToResolve#other" = "**{count}** dialogs to resolve";
|
||||
"ayu_FiltersClearPopupTitle" = "Clear filters";
|
||||
"ayu_FiltersClearPopupText" = "Are you sure you want to clear all filters?";
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "ayu/ayu_worker.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "features/translator/ayu_translator.h"
|
||||
#include "features/filters/shadow_ban_utils.h"
|
||||
#include "lang/lang_instance.h"
|
||||
#include "utils/rc_manager.h"
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ rpl::variable<int> showPeerIdReactive;
|
||||
|
||||
rpl::variable<QString> translationProviderReactive;
|
||||
|
||||
rpl::variable<bool> hideFromBlockedReactive;
|
||||
rpl::event_stream<> filtersUpdateReactive; // triggered on adding / editing filter
|
||||
|
||||
rpl::event_stream<> historyUpdateReactive;
|
||||
|
||||
rpl::lifetime lifetime = rpl::lifetime();
|
||||
@@ -142,8 +143,6 @@ void postinitialize() {
|
||||
showPeerIdReactive = settings->showPeerId;
|
||||
translationProviderReactive = settings->translationProvider;
|
||||
|
||||
hideFromBlockedReactive = settings->hideFromBlocked;
|
||||
|
||||
ghostModeEnabled = ghostModeEnabled_util(settings.value());
|
||||
}
|
||||
|
||||
@@ -226,6 +225,8 @@ AyuGramSettings::AyuGramSettings() {
|
||||
saveForBots = false;
|
||||
|
||||
// ~ Message filters
|
||||
filtersEnabled = false;
|
||||
filtersEnabledInChats = false;
|
||||
hideFromBlocked = false;
|
||||
|
||||
// ~ QoL toggles
|
||||
@@ -257,7 +258,7 @@ AyuGramSettings::AyuGramSettings() {
|
||||
#else
|
||||
AyuAssets::DEFAULT_ICON
|
||||
#endif
|
||||
;
|
||||
;
|
||||
simpleQuotesAndReplies = true;
|
||||
hideFastShare = false;
|
||||
replaceBottomInfoWithIcons = true;
|
||||
@@ -274,6 +275,7 @@ AyuGramSettings::AyuGramSettings() {
|
||||
showHideMessageInContextMenu = 0;
|
||||
showUserMessagesInContextMenu = 2;
|
||||
showMessageDetailsInContextMenu = 2;
|
||||
showAddFilterInContextMenu = 1;
|
||||
|
||||
showAttachButtonInMessageField = true;
|
||||
showCommandsButtonInMessageField = true;
|
||||
@@ -397,9 +399,16 @@ void set_saveForBots(bool val) {
|
||||
settings->saveForBots = val;
|
||||
}
|
||||
|
||||
void set_filtersEnabled(bool val) {
|
||||
settings->filtersEnabled = val;
|
||||
}
|
||||
|
||||
void set_filtersEnabledInChats(bool val) {
|
||||
settings->filtersEnabledInChats = val;
|
||||
}
|
||||
|
||||
void set_hideFromBlocked(bool val) {
|
||||
settings->hideFromBlocked = val;
|
||||
hideFromBlockedReactive = val;
|
||||
}
|
||||
|
||||
void set_disableAds(bool val) {
|
||||
@@ -516,6 +525,10 @@ void set_showMessageDetailsInContextMenu(int val) {
|
||||
settings->showMessageDetailsInContextMenu = val;
|
||||
}
|
||||
|
||||
void set_showAddFilterInContextMenu(int val) {
|
||||
settings->showAddFilterInContextMenu = val;
|
||||
}
|
||||
|
||||
void set_showAttachButtonInMessageField(bool val) {
|
||||
settings->showAttachButtonInMessageField = val;
|
||||
triggerHistoryUpdate();
|
||||
@@ -694,8 +707,12 @@ rpl::producer<bool> get_ghostModeEnabledReactive() {
|
||||
return ghostModeEnabled.value();
|
||||
}
|
||||
|
||||
rpl::producer<bool> get_hideFromBlockedReactive() {
|
||||
return hideFromBlockedReactive.value();
|
||||
void fire_filtersUpdate() {
|
||||
filtersUpdateReactive.fire({});
|
||||
}
|
||||
|
||||
rpl::producer<> get_filtersUpdate() {
|
||||
return filtersUpdateReactive.events();
|
||||
}
|
||||
|
||||
void triggerHistoryUpdate() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include <unordered_set>
|
||||
#include "ayu/libs/json.hpp"
|
||||
#include "ayu/libs/json_ext.hpp"
|
||||
|
||||
@@ -59,6 +60,9 @@ public:
|
||||
|
||||
bool saveForBots;
|
||||
|
||||
std::unordered_set<long long> shadowBanIds;
|
||||
bool filtersEnabled;
|
||||
bool filtersEnabledInChats;
|
||||
bool hideFromBlocked;
|
||||
|
||||
bool disableAds;
|
||||
@@ -95,6 +99,7 @@ public:
|
||||
int showHideMessageInContextMenu;
|
||||
int showUserMessagesInContextMenu;
|
||||
int showMessageDetailsInContextMenu;
|
||||
int showAddFilterInContextMenu;
|
||||
|
||||
bool showAttachButtonInMessageField;
|
||||
bool showCommandsButtonInMessageField;
|
||||
@@ -160,6 +165,8 @@ void set_saveMessagesHistory(bool val);
|
||||
|
||||
void set_saveForBots(bool val);
|
||||
|
||||
void set_filtersEnabled(bool val);
|
||||
void set_filtersEnabledInChats(bool val);
|
||||
void set_hideFromBlocked(bool val);
|
||||
|
||||
void set_disableAds(bool val);
|
||||
@@ -196,6 +203,7 @@ void set_showViewsPanelInContextMenu(int val);
|
||||
void set_showHideMessageInContextMenu(int val);
|
||||
void set_showUserMessagesInContextMenu(int val);
|
||||
void set_showMessageDetailsInContextMenu(int val);
|
||||
void set_showAddFilterInContextMenu(int val);
|
||||
|
||||
void set_showAttachButtonInMessageField(bool val);
|
||||
void set_showCommandsButtonInMessageField(bool val);
|
||||
@@ -255,6 +263,9 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
|
||||
NLOHMANN_JSON_TO(saveDeletedMessages)
|
||||
NLOHMANN_JSON_TO(saveMessagesHistory)
|
||||
NLOHMANN_JSON_TO(saveForBots)
|
||||
NLOHMANN_JSON_TO(shadowBanIds)
|
||||
NLOHMANN_JSON_TO(filtersEnabled)
|
||||
NLOHMANN_JSON_TO(filtersEnabledInChats)
|
||||
NLOHMANN_JSON_TO(hideFromBlocked)
|
||||
NLOHMANN_JSON_TO(disableAds)
|
||||
NLOHMANN_JSON_TO(disableStories)
|
||||
@@ -282,6 +293,7 @@ inline void to_json(nlohmann::json &nlohmann_json_j, const AyuGramSettings &nloh
|
||||
NLOHMANN_JSON_TO(showHideMessageInContextMenu)
|
||||
NLOHMANN_JSON_TO(showUserMessagesInContextMenu)
|
||||
NLOHMANN_JSON_TO(showMessageDetailsInContextMenu)
|
||||
NLOHMANN_JSON_TO(showAddFilterInContextMenu)
|
||||
NLOHMANN_JSON_TO(showAttachButtonInMessageField)
|
||||
NLOHMANN_JSON_TO(showCommandsButtonInMessageField)
|
||||
NLOHMANN_JSON_TO(showEmojiButtonInMessageField)
|
||||
@@ -334,6 +346,9 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveDeletedMessages)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveMessagesHistory)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(saveForBots)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabled)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(filtersEnabledInChats)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(shadowBanIds)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(hideFromBlocked)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(disableAds)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(disableStories)
|
||||
@@ -361,6 +376,7 @@ inline void from_json(const nlohmann::json &nlohmann_json_j, AyuGramSettings &nl
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showHideMessageInContextMenu)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showUserMessagesInContextMenu)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showMessageDetailsInContextMenu)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAddFilterInContextMenu)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showAttachButtonInMessageField)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showCommandsButtonInMessageField)
|
||||
NLOHMANN_JSON_FROM_WITH_DEFAULT(showEmojiButtonInMessageField)
|
||||
@@ -418,7 +434,8 @@ bool isUseScheduledMessages();
|
||||
|
||||
rpl::producer<bool> get_ghostModeEnabledReactive();
|
||||
|
||||
rpl::producer<bool> get_hideFromBlockedReactive();
|
||||
void fire_filtersUpdate();
|
||||
rpl::producer<> get_filtersUpdate();
|
||||
|
||||
void triggerHistoryUpdate();
|
||||
rpl::producer<> get_historyUpdateReactive();
|
||||
|
||||
@@ -111,7 +111,7 @@ auto storage = make_storage(
|
||||
),
|
||||
make_table<RegexFilter>(
|
||||
"RegexFilter",
|
||||
make_column("id", &RegexFilter::id),
|
||||
make_column("id", &RegexFilter::id, primary_key()),
|
||||
make_column("text", &RegexFilter::text),
|
||||
make_column("enabled", &RegexFilter::enabled),
|
||||
make_column("reversed", &RegexFilter::reversed),
|
||||
@@ -276,5 +276,195 @@ bool hasDeletedMessages(ID userId, ID dialogId, ID topicId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
template <typename T>
|
||||
std::vector<T> getAllT() {
|
||||
try {
|
||||
return storage.get_all<T>();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get all: %1").arg(ex.what()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RegexFilter> getAllRegexFilters() {
|
||||
return getAllT<RegexFilter>();
|
||||
}
|
||||
|
||||
std::vector<RegexFilterGlobalExclusion> getAllFiltersExclusions() {
|
||||
return getAllT<RegexFilterGlobalExclusion>();
|
||||
}
|
||||
|
||||
std::vector<RegexFilter> getExcludedByDialogId(ID dialogId) {
|
||||
try {
|
||||
return storage.get_all<RegexFilter>(
|
||||
where(in(&RegexFilter::id,
|
||||
storage.select(columns(&RegexFilterGlobalExclusion::filterId),
|
||||
where(is_equal(&RegexFilterGlobalExclusion::dialogId, dialogId))
|
||||
)
|
||||
))
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get excluded by dialog id: %1").arg(ex.what()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
int getCount() {
|
||||
try {
|
||||
return storage.count<RegexFilter>();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get count: %1").arg(ex.what()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
RegexFilter getById(std::vector<char> id) {
|
||||
try {
|
||||
return storage.get<RegexFilter>(
|
||||
where(column<RegexFilter>(&RegexFilter::id) == std::move(id))
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get filters by id: %1").arg(ex.what()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RegexFilter> getShared() {
|
||||
try {
|
||||
return storage.get_all<RegexFilter>(
|
||||
where(is_null(column<RegexFilter>(&RegexFilter::dialogId)))
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get shared filters: %1").arg(ex.what()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RegexFilter> getByDialogId(ID dialogId) {
|
||||
try {
|
||||
return storage.get_all<RegexFilter>(
|
||||
where(column<RegexFilter>(&RegexFilter::dialogId) == dialogId)
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to get filters by dialog id: %1").arg(ex.what()));
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void addRegexFilter(const RegexFilter &filter) {
|
||||
try {
|
||||
storage.begin_transaction();
|
||||
storage.replace(filter); // we're using replace as we set std::vector<char> as primary key
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to save regex filter for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion) {
|
||||
try {
|
||||
storage.begin_transaction();
|
||||
storage.insert(exclusion);
|
||||
storage.commit();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to save regex filter exclusion for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void updateRegexFilter(const RegexFilter &filter) {
|
||||
try {
|
||||
storage.update_all(
|
||||
set(
|
||||
c(&RegexFilter::text) = filter.text,
|
||||
c(&RegexFilter::enabled) = filter.enabled,
|
||||
c(&RegexFilter::reversed) = filter.reversed,
|
||||
c(&RegexFilter::caseInsensitive) = filter.caseInsensitive,
|
||||
c(&RegexFilter::dialogId) = filter.dialogId
|
||||
),
|
||||
where(c(&RegexFilter::id) == filter.id)
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to update regex filter for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteFilter(const std::vector<char> &id) {
|
||||
try {
|
||||
storage.remove_all<RegexFilter>(
|
||||
where(column<RegexFilter>(&RegexFilter::id) == id)
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to delete regex filter for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteExclusionsByFilterId(const std::vector<char> &id) {
|
||||
try {
|
||||
storage.remove_all<RegexFilterGlobalExclusion>(
|
||||
where(column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::filterId) == id)
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to delete regex filter exclusion by filter id for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteExclusion(ID dialogId, std::vector<char> filterId) {
|
||||
try {
|
||||
storage.remove_all<RegexFilterGlobalExclusion>(
|
||||
where(column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::filterId) == filterId and
|
||||
column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::dialogId) == dialogId
|
||||
)
|
||||
);
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to delete regex filter exclusion for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteAllFilters() {
|
||||
try {
|
||||
storage.remove_all<RegexFilter>();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to delete all regex filter for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void deleteAllExclusions() {
|
||||
try {
|
||||
storage.remove_all<RegexFilterGlobalExclusion>();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to delete all regex filter exclusions for some reason: %1").arg(ex.what()));
|
||||
}
|
||||
}
|
||||
|
||||
bool hasFilters() {
|
||||
try {
|
||||
return !storage.select(
|
||||
columns(column<RegexFilter>(&RegexFilter::id)),
|
||||
limit(1)
|
||||
).empty();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to check if there's any filters: %1").arg(ex.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool hasPerDialogFilters() {
|
||||
try {
|
||||
return
|
||||
!storage.select(
|
||||
columns(column<RegexFilter>(&RegexFilter::id)),
|
||||
where(is_not_null(column<RegexFilter>(&RegexFilter::dialogId))),
|
||||
limit(1)
|
||||
).empty() ||
|
||||
!storage.select(
|
||||
columns(column<RegexFilterGlobalExclusion>(&RegexFilterGlobalExclusion::fakeId)),
|
||||
limit(1)
|
||||
).empty();
|
||||
} catch (std::exception &ex) {
|
||||
LOG(("Failed to check if there's any filters: %1").arg(ex.what()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,4 +20,29 @@ void addDeletedMessage(const DeletedMessage &message);
|
||||
std::vector<DeletedMessage> getDeletedMessages(ID userId, ID dialogId, ID topicId, ID minId, ID maxId, int totalLimit);
|
||||
bool hasDeletedMessages(ID userId, ID dialogId, ID topicId);
|
||||
|
||||
std::vector<RegexFilter> getAllRegexFilters();
|
||||
RegexFilter getById(std::vector<char> id);
|
||||
std::vector<RegexFilter> getShared();
|
||||
std::vector<RegexFilter> getByDialogId(ID dialogId);
|
||||
std::vector<RegexFilterGlobalExclusion> getAllFiltersExclusions();
|
||||
std::vector<RegexFilter> getExcludedByDialogId(ID dialogId);
|
||||
|
||||
int getCount();
|
||||
|
||||
|
||||
void addRegexFilter(const RegexFilter &filter);
|
||||
void addRegexExclusion(const RegexFilterGlobalExclusion &exclusion);
|
||||
|
||||
void updateRegexFilter(const RegexFilter &filter);
|
||||
|
||||
void deleteFilter(const std::vector<char> &id);
|
||||
void deleteExclusionsByFilterId(const std::vector<char> &id);
|
||||
void deleteExclusion(ID dialogId, std::vector<char> filterId);
|
||||
|
||||
void deleteAllFilters();
|
||||
void deleteAllExclusions();
|
||||
|
||||
bool hasFilters();
|
||||
bool hasPerDialogFilters();
|
||||
|
||||
}
|
||||
|
||||
@@ -80,7 +80,28 @@ public:
|
||||
bool enabled;
|
||||
bool reversed;
|
||||
bool caseInsensitive;
|
||||
std::unique_ptr<ID> dialogId; // nullable
|
||||
std::optional<ID> dialogId; // nullable
|
||||
|
||||
bool operator==(const RegexFilter &other) const {
|
||||
return id == other.id &&
|
||||
text == other.text &&
|
||||
caseInsensitive == other.caseInsensitive &&
|
||||
reversed == other.reversed &&
|
||||
dialogId == other.dialogId &&
|
||||
enabled == other.enabled;
|
||||
}
|
||||
[[nodiscard]] QJsonObject toJson() const {
|
||||
QJsonObject json;
|
||||
json["id"] = QString::fromUtf8(id.data());
|
||||
json["text"] = QString::fromStdString(text);
|
||||
json["enabled"] = enabled;
|
||||
json["reversed"] = reversed;
|
||||
json["caseInsensitive"] = caseInsensitive;
|
||||
if (dialogId.has_value()) {
|
||||
json["dialogId"] = dialogId.value();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
class RegexFilterGlobalExclusion
|
||||
@@ -89,6 +110,10 @@ public:
|
||||
ID fakeId;
|
||||
ID dialogId;
|
||||
std::vector<char> filterId;
|
||||
|
||||
bool operator==(const RegexFilterGlobalExclusion& other) const {
|
||||
return dialogId == other.dialogId && filterId == other.filterId;
|
||||
}
|
||||
};
|
||||
|
||||
class SpyMessageRead
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "filters_cache_controller.h"
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
#include "filters_controller.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
|
||||
static std::mutex mutex;
|
||||
|
||||
namespace FiltersCacheController {
|
||||
std::optional<std::vector<HashablePattern>> sharedPatterns;
|
||||
std::optional<std::unordered_map<long long, std::vector<ReversiblePattern>>> patternsByDialogId;
|
||||
|
||||
std::optional<std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>>> exclusionsByDialogId;
|
||||
|
||||
std::unordered_map<long long, std::unordered_map<std::optional<int>,std::optional<bool>>> filteredMessages;
|
||||
|
||||
|
||||
void rebuildCache() {
|
||||
std::lock_guard lock(mutex);
|
||||
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
std::vector<HashablePattern> shared;
|
||||
std::unordered_map<long long, std::vector<ReversiblePattern>> byDialogId;
|
||||
|
||||
for (const auto &filter : filters) {
|
||||
if (!filter.enabled || filter.text.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int flags = UREGEX_MULTILINE;
|
||||
if (filter.caseInsensitive)
|
||||
flags |= UREGEX_CASE_INSENSITIVE;
|
||||
|
||||
|
||||
auto status = U_ZERO_ERROR;
|
||||
auto pattern = RegexPattern::compile(UnicodeString::fromUTF8(filter.text), flags, status);
|
||||
|
||||
if (!pattern) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter.dialogId.has_value()) {
|
||||
byDialogId[filter.dialogId.value()].push_back({std::shared_ptr<RegexPattern>(pattern), filter.reversed});
|
||||
} else {
|
||||
shared.push_back({filter.id, {std::shared_ptr<RegexPattern>(pattern), filter.reversed}});
|
||||
}
|
||||
}
|
||||
|
||||
auto exclByDialogId = buildExclusions(exclusions, shared);
|
||||
|
||||
sharedPatterns = shared;
|
||||
patternsByDialogId = byDialogId;
|
||||
exclusionsByDialogId = exclByDialogId;
|
||||
filteredMessages.clear();
|
||||
}
|
||||
|
||||
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> buildExclusions(
|
||||
const std::vector<RegexFilterGlobalExclusion> &exclusions,
|
||||
const std::vector<HashablePattern> &shared) {
|
||||
|
||||
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> exclusionsByDialogId;
|
||||
|
||||
for (const auto &exclusion : exclusions) {
|
||||
|
||||
auto &exclusionSet = exclusionsByDialogId[exclusion.dialogId];
|
||||
|
||||
for (const auto &filter : shared) {
|
||||
if (filter.id == exclusion.filterId) {
|
||||
|
||||
exclusionSet.insert(filter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return exclusionsByDialogId;
|
||||
}
|
||||
|
||||
std::optional<bool> isFiltered(not_null<HistoryItem *> item) {
|
||||
std::lock_guard lock(mutex);
|
||||
auto dialogIt = filteredMessages.find(item->history()->peer->id.value);
|
||||
|
||||
if (dialogIt == filteredMessages.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto it = dialogIt->second.find(item->id.bare);
|
||||
if (it == dialogIt->second.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void putFiltered(not_null<HistoryItem *> item, bool res) {
|
||||
std::lock_guard lock(mutex);
|
||||
filteredMessages[item->history()->peer->id.value][item->id.bare] = res;
|
||||
}
|
||||
|
||||
std::optional<std::vector<ReversiblePattern>> getPatternsByDialogId(uint64 dialogId) {
|
||||
if (!patternsByDialogId.has_value()) {
|
||||
rebuildCache();
|
||||
}
|
||||
const auto it = patternsByDialogId.value().find(dialogId);
|
||||
if (it == patternsByDialogId.value().end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::optional<const std::unordered_set<HashablePattern, PatternHasher>> getExclusionsByDialogId(long long dialogId) {
|
||||
std::lock_guard lock(mutex);
|
||||
|
||||
if (!exclusionsByDialogId.has_value()) {
|
||||
rebuildCache();
|
||||
}
|
||||
const auto it = exclusionsByDialogId.value().find(dialogId);
|
||||
if (it == exclusionsByDialogId.value().end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
const std::vector<HashablePattern> &getSharedPatterns() {
|
||||
if (!sharedPatterns.has_value()) {
|
||||
rebuildCache();
|
||||
}
|
||||
return sharedPatterns.value();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "filters_controller.h"
|
||||
#include "ayu/data/entities.h"
|
||||
|
||||
using namespace FiltersController;
|
||||
|
||||
namespace FiltersCacheController {
|
||||
|
||||
|
||||
void rebuildCache();
|
||||
|
||||
std::unordered_map<long long, std::unordered_set<HashablePattern, PatternHasher>> buildExclusions(
|
||||
const std::vector<RegexFilterGlobalExclusion>& exclusions,
|
||||
const std::vector<HashablePattern>& shared);
|
||||
|
||||
std::optional<bool> isFiltered(not_null<HistoryItem *> item);
|
||||
void putFiltered(not_null<HistoryItem *> item, bool res);
|
||||
|
||||
std::optional<std::vector<ReversiblePattern>> getPatternsByDialogId(uint64 dialogId);
|
||||
std::optional<const std::unordered_set<HashablePattern, PatternHasher>> getExclusionsByDialogId(long long dialogId);
|
||||
const std::vector<HashablePattern> &getSharedPatterns();
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
|
||||
#include "filters_controller.h"
|
||||
|
||||
#include "filters_cache_controller.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item.h"
|
||||
#include "unicode/regex.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include "ayu/data/entities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_peer_id.h"
|
||||
|
||||
#include "data/data_session.h"
|
||||
#include "history/history_item_components.h"
|
||||
|
||||
#include "filters_utils.h"
|
||||
#include "shadow_ban_utils.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
|
||||
namespace FiltersController {
|
||||
|
||||
bool filterBlocked(const not_null<HistoryItem*> item) {
|
||||
if (item->from() != item->history()->peer) {
|
||||
if (isBlocked(item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::optional<bool> isFiltered(const QString &str, uint64 dialogId) {
|
||||
if (str.isEmpty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto icuStr = UnicodeString(reinterpret_cast<const UChar*>(str.constData()), str.length());
|
||||
|
||||
const auto matches = [&](const ReversiblePattern &pattern)
|
||||
{
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
auto match = pattern.pattern->matcher(icuStr, status)->find();
|
||||
if (U_FAILURE(status)) {
|
||||
LOG(("FILTER FAILED: %1").arg(u_errorName(status)));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto reversed = pattern.reversed;
|
||||
|
||||
if (!reversed && match || reversed && !match) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (const auto &dialogPatterns = FiltersCacheController::getPatternsByDialogId(dialogId); dialogPatterns.has_value() && !dialogPatterns.value().empty()) {
|
||||
for (const auto &pattern : dialogPatterns.value()) {
|
||||
if (matches(pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto &exclusions = FiltersCacheController::getExclusionsByDialogId(dialogId);
|
||||
if (const auto &sharedPatterns = FiltersCacheController::getSharedPatterns(); !sharedPatterns.empty()) {
|
||||
for (const auto &pattern : sharedPatterns) {
|
||||
if (exclusions.has_value() && exclusions.value().contains(pattern)) {
|
||||
continue;
|
||||
}
|
||||
if (matches(pattern.pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isEnabled(not_null<PeerData*> peer) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
return settings.filtersEnabled && (settings.filtersEnabledInChats || !peer->isMegagroup() && !peer->isGigagroup());
|
||||
}
|
||||
|
||||
bool isBlocked(const not_null<HistoryItem*> item) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
|
||||
const auto blocked = [&]() -> bool
|
||||
{
|
||||
if (item->from()->isUser() &&
|
||||
item->from()->asUser()->isBlocked()) {
|
||||
// don't hide messages if it's a dialog with blocked user
|
||||
return item->from()->asUser()->id != item->history()->peer->id;
|
||||
}
|
||||
|
||||
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
|
||||
if (forwarded->originalSender &&
|
||||
forwarded->originalSender->isUser() &&
|
||||
forwarded->originalSender->asUser()->isBlocked()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}();
|
||||
|
||||
return settings.filtersEnabled &&
|
||||
(
|
||||
item->from()->isUser() && ShadowBanUtils::isShadowBanned(getDialogIdFromPeer(item->from())) ||
|
||||
settings.hideFromBlocked && blocked
|
||||
);
|
||||
}
|
||||
|
||||
bool filtered(const not_null<HistoryItem*> item) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
if (!settings.filtersEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item->out()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterBlocked(item)) return true;
|
||||
|
||||
if (!isEnabled(item->history()->peer)) return false;
|
||||
|
||||
const auto cached = FiltersCacheController::isFiltered(item);
|
||||
if (cached.has_value()) {
|
||||
return cached.value();
|
||||
}
|
||||
const auto res = isFiltered(FilterUtils::extractAllText(item), getDialogIdFromPeer(item->history()->peer));
|
||||
|
||||
// sometimes item has empty text.
|
||||
// so we cache result only if
|
||||
// processed item is filterable
|
||||
if (res.has_value()) {
|
||||
FiltersCacheController::putFiltered(item, res.value());
|
||||
return res.value();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "unicode/regex.h"
|
||||
|
||||
using namespace icu_78;
|
||||
|
||||
namespace FiltersController {
|
||||
|
||||
bool isEnabled(PeerData *peer);
|
||||
bool isBlocked(not_null<HistoryItem*> item);
|
||||
bool filteredWithoutCaching(not_null<HistoryItem*> historyItem);
|
||||
bool filtered(not_null<HistoryItem*> historyItem);
|
||||
|
||||
struct ReversiblePattern
|
||||
{
|
||||
std::shared_ptr<RegexPattern> pattern;
|
||||
bool reversed;
|
||||
};
|
||||
|
||||
struct HashablePattern
|
||||
{
|
||||
std::vector<char> id;
|
||||
ReversiblePattern pattern;
|
||||
|
||||
bool operator==(const HashablePattern &other) const {
|
||||
return id == other.id;
|
||||
}
|
||||
};
|
||||
|
||||
struct PatternHasher
|
||||
{
|
||||
std::size_t operator()(const HashablePattern &p) const {
|
||||
std::string_view view(p.id.data(), p.id.size());
|
||||
return std::hash<std::string_view>{}(view);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "filters_utils.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QGuiApplication>
|
||||
#include <QJsonArray>
|
||||
#include <qjsondocument.h>
|
||||
#include <QString>
|
||||
#include <vector>
|
||||
#include <QtNetwork/QHttpPart>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include "filters_cache_controller.h"
|
||||
#include "lang_auto.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_document.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "history/history_item.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_domain.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/toast/toast.h"
|
||||
|
||||
constexpr auto BACKUP_VERSION = 2;
|
||||
|
||||
void FilterUtils::importFromLink(const QString &link) {
|
||||
if (link.isEmpty()) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto request = QNetworkRequest(QUrl(link));
|
||||
_reply = _manager->get(request);
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[=]
|
||||
{
|
||||
const auto responseData = _reply->readAll();
|
||||
|
||||
const auto jsonString = QString::fromUtf8(responseData);
|
||||
|
||||
if (jsonString.isNull()) {
|
||||
LOG(("FilterUtils: Invalid response."));
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
|
||||
_reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!handleResponse(jsonString.toUtf8())) {
|
||||
LOG(("FilterUtils: Error handling response."));
|
||||
}
|
||||
_reply->deleteLater();
|
||||
});
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
&QNetworkReply::errorOccurred,
|
||||
this,
|
||||
[=](QNetworkReply::NetworkError e)
|
||||
{
|
||||
gotFailure(e);
|
||||
|
||||
_reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void FilterUtils::publishFilters() {
|
||||
const auto exported = exportFilters();
|
||||
|
||||
auto multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
|
||||
QHttpPart contentPart;
|
||||
contentPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"content\""));
|
||||
contentPart.setBody(exported.toUtf8());
|
||||
|
||||
QHttpPart syntaxPart;
|
||||
syntaxPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"syntax\""));
|
||||
syntaxPart.setBody("json");
|
||||
|
||||
QHttpPart titlePart;
|
||||
titlePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"title\""));
|
||||
titlePart.setBody("AyuGram Filters");
|
||||
|
||||
multiPart->append(contentPart);
|
||||
multiPart->append(syntaxPart);
|
||||
multiPart->append(titlePart);
|
||||
|
||||
QNetworkRequest request(QUrl("https://dpaste.com/api/v2/"));
|
||||
|
||||
_reply = _manager->post(request, multiPart);
|
||||
multiPart->setParent(_reply);
|
||||
|
||||
connect(
|
||||
_reply,
|
||||
&QNetworkReply::finished,
|
||||
this,
|
||||
[=]
|
||||
{
|
||||
const auto error = _reply->error();
|
||||
const auto location = _reply->header(QNetworkRequest::LocationHeader);
|
||||
|
||||
if (error == QNetworkReply::NoError && location.isValid()) {
|
||||
auto url = location.toString();
|
||||
url.append(".txt");
|
||||
QGuiApplication::clipboard()->setText(url);
|
||||
|
||||
Ui::Toast::Show(tr::lng_stickers_copied(tr::now));
|
||||
} else {
|
||||
LOG(("Failed to publish filters to dpaste, error: %1").arg(_reply->errorString()));
|
||||
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailPublish(tr::now));
|
||||
}
|
||||
_reply->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
bool FilterUtils::importFromJson(const QByteArray &json) {
|
||||
auto error = QJsonParseError{0, QJsonParseError::NoError};
|
||||
const auto document = QJsonDocument::fromJson(json, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
LOG(("FilterUtils: Failed to parse JSON, error: %1"
|
||||
).arg(error.errorString()));
|
||||
return false;
|
||||
}
|
||||
if (!document.isObject()) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
LOG(("FilterUtils: not an object received in JSON"));
|
||||
return false;
|
||||
}
|
||||
const auto changes = prepareChanges(document.object());
|
||||
|
||||
if (changes == ApplyChanges{}) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailImport(tr::now));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto any = !changes.newFilters.empty() || !changes.removeFiltersById.empty() || !changes.filtersOverrides.
|
||||
empty() || !changes.newExclusions.empty() || !changes.removeExclusions.empty() || !changes.peersToBeResolved.
|
||||
empty();
|
||||
|
||||
if (!any) {
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailNoChanges(tr::now));
|
||||
return false;
|
||||
}
|
||||
|
||||
applyChanges(changes);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct BackupExclusion
|
||||
{
|
||||
ID dialogId;
|
||||
std::vector<char> filterId;
|
||||
|
||||
QJsonObject toJson() const {
|
||||
QJsonObject json;
|
||||
json["dialogId"] = dialogId;
|
||||
|
||||
// make it look like java's UUID
|
||||
auto hexId = QString(QByteArray(filterId.data(), filterId.size()).toHex());
|
||||
if (hexId.length() == 32) {
|
||||
hexId.insert(8, '-');
|
||||
hexId.insert(13, '-');
|
||||
hexId.insert(18, '-');
|
||||
hexId.insert(23, '-');
|
||||
}
|
||||
json["filterId"] = hexId;
|
||||
return json;
|
||||
}
|
||||
};
|
||||
|
||||
QString FilterUtils::exportFilters() {
|
||||
auto createJsonArray = [&](const auto &container)
|
||||
{
|
||||
QJsonArray jsonArray;
|
||||
for (const auto &item : container) {
|
||||
jsonArray.append(item.toJson());
|
||||
}
|
||||
return jsonArray;
|
||||
};
|
||||
|
||||
QJsonArray filtersArray;
|
||||
QJsonObject jsonObject;
|
||||
jsonObject["version"] = BACKUP_VERSION;
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
|
||||
for (const auto &item : filters) {
|
||||
QJsonObject filterJson;
|
||||
filterJson["caseInsensitive"] = item.caseInsensitive;
|
||||
if (item.dialogId.has_value()) {
|
||||
filterJson["dialogId"] = item.dialogId.value();
|
||||
} else {
|
||||
filterJson["dialogId"] = QJsonValue();
|
||||
}
|
||||
filterJson["enabled"] = item.enabled;
|
||||
filterJson["reversed"] = item.reversed;
|
||||
filterJson["text"] = QString::fromStdString(item.text);
|
||||
|
||||
// make it look like java's UUID
|
||||
auto hexId = QString(QByteArray(item.id.data(), item.id.size()).toHex());
|
||||
if (hexId.length() == 32) {
|
||||
hexId.insert(8, '-');
|
||||
hexId.insert(13, '-');
|
||||
hexId.insert(18, '-');
|
||||
hexId.insert(23, '-');
|
||||
}
|
||||
filterJson["id"] = hexId;
|
||||
filtersArray.append(filterJson);
|
||||
}
|
||||
jsonObject["filters"] = filtersArray;
|
||||
|
||||
const auto excl = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
|
||||
std::vector<BackupExclusion> exclusions;
|
||||
exclusions.reserve(excl.size());
|
||||
|
||||
for (const auto &item : excl) {
|
||||
exclusions.push_back(BackupExclusion{item.dialogId, item.filterId});
|
||||
}
|
||||
|
||||
jsonObject["exclusions"] = createJsonArray(exclusions);
|
||||
jsonObject["removeFiltersById"] = QJsonValue();
|
||||
jsonObject["removeExclusions"] = QJsonValue();
|
||||
|
||||
QJsonObject peers;
|
||||
for (const auto &item : filters) {
|
||||
const auto &session = currentSession();
|
||||
if (!item.dialogId.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (const auto peer = session->data().peer(peerFromChat(abs(item.dialogId.value())))) {
|
||||
if (!peer->username().isEmpty()) {
|
||||
QString key = QString::number(item.dialogId.value());
|
||||
peers[key] = peer->username();
|
||||
}
|
||||
}
|
||||
}
|
||||
jsonObject["peers"] = peers;
|
||||
|
||||
|
||||
QJsonDocument jsonDoc(jsonObject);
|
||||
QByteArray jsonData = jsonDoc.toJson(QJsonDocument::Indented);
|
||||
return QString::fromUtf8(jsonData);
|
||||
}
|
||||
|
||||
// for compatibility with Android version
|
||||
|
||||
int typeOfMessage(const HistoryItem *item) {
|
||||
if (item->isSponsored()) {
|
||||
return 0; // TYPE_TEXT
|
||||
}
|
||||
|
||||
if (!item->isService()) {
|
||||
if (const auto media = item->media()) {
|
||||
if (const auto invoice = media->invoice(); invoice && invoice->isPaidMedia) {
|
||||
return 29; // TYPE_PAID_MEDIA
|
||||
}
|
||||
if (media->giveawayStart()) {
|
||||
return 26; // TYPE_GIVEAWAY
|
||||
}
|
||||
if (media->giveawayResults()) {
|
||||
return 28; // TYPE_GIVEAWAY_RESULTS
|
||||
}
|
||||
if (const auto dice = dynamic_cast<Data::MediaDice*>(media)) {
|
||||
return 15; // TYPE_ANIMATED_STICKER
|
||||
}
|
||||
if (media->photo()) {
|
||||
return 1; // TYPE_PHOTO
|
||||
}
|
||||
if (media->location()) {
|
||||
return 4; // TYPE_GEO
|
||||
}
|
||||
if (media->sharedContact()) {
|
||||
return 12; // TYPE_CONTACT
|
||||
}
|
||||
if (media->poll() || media->todolist()) {
|
||||
return 17; // TYPE_POLL
|
||||
}
|
||||
if (media->storyId().valid()) {
|
||||
if (media->storyMention()) {
|
||||
return 24; // TYPE_STORY_MENTION
|
||||
}
|
||||
return 23; // TYPE_STORY
|
||||
}
|
||||
if (const auto document = media->document()) {
|
||||
if (document->round()) {
|
||||
return 5; // TYPE_ROUND_VIDEO
|
||||
}
|
||||
if (document->isVideoFile()) {
|
||||
return 3; // TYPE_VIDEO
|
||||
}
|
||||
if (document->isVoiceMessage()) {
|
||||
return 2; // TYPE_VOICE
|
||||
}
|
||||
if (document->isAudioFile()) {
|
||||
return 14; // TYPE_MUSIC
|
||||
}
|
||||
if (document->isAnimation()) {
|
||||
return 8; // TYPE_GIF
|
||||
}
|
||||
if (document->sticker()) {
|
||||
if (document->isAnimation()) {
|
||||
return 15; // TYPE_ANIMATED_STICKER
|
||||
}
|
||||
return 13; // TYPE_STICKER
|
||||
}
|
||||
return 9; // TYPE_FILE
|
||||
}
|
||||
if (media->game() || media->invoice() || media->webpage()) {
|
||||
return 0; // TYPE_TEXT
|
||||
}
|
||||
} else {
|
||||
if (item->isOnlyEmojiAndSpaces()) {
|
||||
return 19; // TYPE_EMOJIS
|
||||
}
|
||||
return 0; // TYPE_TEXT
|
||||
}
|
||||
} else {
|
||||
if (const auto media = item->media()) {
|
||||
if (media->call()) {
|
||||
return 16; // TYPE_PHONE_CALL
|
||||
}
|
||||
if (media->photo() && !item->isUserpicSuggestion()) {
|
||||
return 11; // TYPE_ACTION_PHOTO
|
||||
}
|
||||
if (media->photo() && item->isUserpicSuggestion()) {
|
||||
return 21; // TYPE_SUGGEST_PHOTO
|
||||
}
|
||||
if (media->paper()) {
|
||||
return 22; // TYPE_ACTION_WALLPAPER
|
||||
}
|
||||
if (const auto gift = media->gift()) {
|
||||
if (gift->type == Data::GiftType::Premium) {
|
||||
if (gift->channel) {
|
||||
return 25; // TYPE_GIFT_PREMIUM_CHANNEL
|
||||
}
|
||||
return 18; // TYPE_GIFT_PREMIUM
|
||||
}
|
||||
if (gift->type == Data::GiftType::Credits
|
||||
|| gift->type == Data::GiftType::StarGift
|
||||
|| gift->type == Data::GiftType::Ton) {
|
||||
return 30; // TYPE_GIFT_STARS
|
||||
}
|
||||
}
|
||||
if (item->Get<HistoryServiceGiveawayResults>()) {
|
||||
return 28; // TYPE_GIVEAWAY_RESULTS
|
||||
}
|
||||
}
|
||||
return 10; // TYPE_DATE
|
||||
}
|
||||
return 0; // TYPE_TEXT
|
||||
}
|
||||
|
||||
QString FilterUtils::extractAllText(not_null<HistoryItem*> item) {
|
||||
QString text(item->originalText().text);
|
||||
if (!item->originalText().entities.empty()) {
|
||||
for (const auto &entity : item->originalText().entities) {
|
||||
if (entity.type() == EntityType::Url || entity.type() == EntityType::CustomUrl) {
|
||||
text.append("\n");
|
||||
text.append(entity.data());
|
||||
}
|
||||
}
|
||||
text.append("\n");
|
||||
}
|
||||
|
||||
if (const auto markup = item->Get<HistoryMessageReplyMarkup>()) {
|
||||
if (!markup->data.isNull()) {
|
||||
for (const auto &row : markup->data.rows) {
|
||||
for (const auto &button : row) {
|
||||
text.append("<button>").append(button.text).append(" ").append(qs(button.data)).append("</button>");
|
||||
text.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.append("\n").append("<type>").append(QString::number(typeOfMessage(item))).append("</type>");
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
bool FilterUtils::handleResponse(const QByteArray &response) {
|
||||
try {
|
||||
return importFromJson(response);
|
||||
} catch (...) {
|
||||
LOG(("FilterUtils: Failed to apply response"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FilterUtils::gotFailure(const QNetworkReply::NetworkError &error) {
|
||||
LOG(("FilterUtils: Error %1").arg(error));
|
||||
Ui::Toast::Show(tr::ayu_FiltersToastFailFetch(tr::now));
|
||||
}
|
||||
|
||||
ApplyChanges FilterUtils::prepareChanges(const QJsonObject &root) {
|
||||
const auto version = root.value("version");
|
||||
|
||||
if (version.toInt() > BACKUP_VERSION) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
const auto existingFilters = AyuDatabase::getAllRegexFilters();
|
||||
const auto existingExclusions = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
std::vector<RegexFilter> filtersOverrides;
|
||||
std::map<std::vector<char>, RegexFilter> newFilters;
|
||||
std::vector<RegexFilterGlobalExclusion> newExclusions;
|
||||
std::vector<QString> removeFiltersById;
|
||||
std::vector<RegexFilterGlobalExclusion> removeExclusions;
|
||||
std::map<long long, QString> peersToBeResolved;
|
||||
|
||||
|
||||
if (const auto &filters = root.value("filters").toArray(); !filters.isEmpty()) {
|
||||
for (const auto &filterRef : filters) {
|
||||
if (const auto filter = filterRef.toObject(); !filter.isEmpty()) {
|
||||
RegexFilter regex;
|
||||
regex.caseInsensitive = filter.value("caseInsensitive").toBool();
|
||||
|
||||
const auto dialogIdValue = filter.value("dialogId");
|
||||
if (!dialogIdValue.isNull()) {
|
||||
regex.dialogId = filter.value("dialogId").toVariant().toLongLong();
|
||||
} else {
|
||||
regex.dialogId = std::nullopt;
|
||||
}
|
||||
regex.enabled = filter.value("enabled").toBool();
|
||||
|
||||
auto idString = filter.value("id").toString();
|
||||
idString.remove('-');
|
||||
|
||||
auto idBytes = QByteArray::fromHex(idString.toUtf8());
|
||||
regex.id = std::vector(idBytes.constData(), idBytes.constData() + idBytes.size());
|
||||
|
||||
regex.reversed = filter.value("reversed").toBool();
|
||||
regex.text = filter.value("text").toString().toStdString();
|
||||
|
||||
|
||||
auto it = std::ranges::find_if(existingFilters,
|
||||
[®ex](const RegexFilter &f)
|
||||
{
|
||||
return f.id == regex.id;
|
||||
});
|
||||
if (it != existingFilters.end()) {
|
||||
const RegexFilter &existing = *it;
|
||||
if (existing != regex) {
|
||||
filtersOverrides.push_back(existing);
|
||||
}
|
||||
} else {
|
||||
newFilters[regex.id] = std::move(regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto exclusions = root.value("exclusions").toArray(); !exclusions.isEmpty()) {
|
||||
for (const auto &exclusionRef : exclusions) {
|
||||
if (const auto exclusion = exclusionRef.toObject(); !exclusion.isEmpty()) {
|
||||
RegexFilterGlobalExclusion regex;
|
||||
|
||||
regex.dialogId = exclusion.value("dialogId").toVariant().toLongLong();
|
||||
|
||||
auto filterIdString = exclusion.value("filterId").toString();
|
||||
filterIdString.remove('-');
|
||||
|
||||
auto filterIdBytes = QByteArray::fromHex(filterIdString.toUtf8());
|
||||
regex.filterId = std::vector(
|
||||
filterIdBytes.constData(),
|
||||
filterIdBytes.constData() + filterIdBytes.size()
|
||||
);
|
||||
|
||||
auto it = std::ranges::find_if(
|
||||
existingExclusions,
|
||||
[®ex](const RegexFilterGlobalExclusion &f)
|
||||
{
|
||||
return f.dialogId == regex.dialogId && f.filterId == regex.filterId;
|
||||
});
|
||||
|
||||
if (it == existingExclusions.end()) {
|
||||
newExclusions.push_back(std::move(regex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto removeFiltersByIdJson = root.value("removeFiltersById").toArray(); !removeFiltersByIdJson.
|
||||
isEmpty()) {
|
||||
for (const auto &filterRef : removeFiltersByIdJson) {
|
||||
const auto filter = filterRef.toString();
|
||||
|
||||
const auto byteArray = filter.toUtf8();
|
||||
const auto filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.size());
|
||||
|
||||
const auto exists = std::ranges::any_of(
|
||||
existingFilters,
|
||||
[&](const RegexFilter &f)
|
||||
{
|
||||
return f.id == filterId;
|
||||
});
|
||||
if (exists) {
|
||||
removeFiltersById.push_back(filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto removeExclusionsJson = root.value("removeExclusions").toArray(); !removeExclusionsJson.isEmpty()) {
|
||||
for (const auto &exclusionRef : removeExclusionsJson) {
|
||||
const auto exclusionObj = exclusionRef.toObject();
|
||||
const auto filterIdStr = exclusionObj.value("filterId").toString();
|
||||
const qint64 dialogId = exclusionObj.value("dialogId").toVariant().toLongLong();
|
||||
|
||||
const auto byteArray = filterIdStr.toUtf8();
|
||||
const auto filterIdVec = std::vector<char>(byteArray.constData(), byteArray.constData() + byteArray.size());
|
||||
|
||||
const bool exists = std::ranges::any_of(
|
||||
existingExclusions,
|
||||
[&](const RegexFilterGlobalExclusion &x)
|
||||
{
|
||||
return x.filterId == filterIdVec && x.dialogId == dialogId;
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
RegexFilterGlobalExclusion regex;
|
||||
regex.dialogId = dialogId;
|
||||
regex.filterId = filterIdVec;
|
||||
removeExclusions.push_back(regex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto peersJson = root.value("peers").toObject(); !peersJson.isEmpty()) {
|
||||
for (const auto &dialogIdStr : peersJson.keys()) {
|
||||
bool parsed;
|
||||
const auto dialogId = dialogIdStr.toLongLong(&parsed);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// almost everytime fails
|
||||
PeerData *peerMaybe = nullptr;
|
||||
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
||||
if (const auto session = account->maybeSession()) {
|
||||
if (const auto peer = session->data().peer(peerFromChat(abs(dialogId)))) {
|
||||
peerMaybe = peer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!peerMaybe) {
|
||||
const auto username = peersJson.value(dialogIdStr).toString();
|
||||
peersToBeResolved[dialogId] = username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ApplyChanges changes;
|
||||
for (auto &filter : newFilters) {
|
||||
changes.newFilters.push_back(std::move(filter.second));
|
||||
}
|
||||
changes.removeFiltersById = std::move(removeFiltersById);
|
||||
changes.filtersOverrides = std::move(filtersOverrides);
|
||||
changes.newExclusions = std::move(newExclusions);
|
||||
changes.removeExclusions = std::move(removeExclusions);
|
||||
changes.peersToBeResolved = std::move(peersToBeResolved);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
void FilterUtils::applyChanges(const ApplyChanges &changes) {
|
||||
if (!changes.newFilters.empty()) {
|
||||
for (const auto &filter : changes.newFilters) {
|
||||
AyuDatabase::addRegexFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.removeFiltersById.empty()) {
|
||||
for (const auto &id : changes.removeFiltersById) {
|
||||
const auto byteArray = id.toUtf8();
|
||||
const auto filterId = std::vector<char>(byteArray.constData(), byteArray.constData() + byteArray.size());
|
||||
AyuDatabase::deleteExclusionsByFilterId(filterId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.filtersOverrides.empty()) {
|
||||
for (const auto &filter : changes.filtersOverrides) {
|
||||
AyuDatabase::updateRegexFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.newExclusions.empty()) {
|
||||
for (const auto &exclusion : changes.newExclusions) {
|
||||
AyuDatabase::addRegexExclusion(exclusion);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.removeExclusions.empty()) {
|
||||
for (const auto &exclusion : changes.removeExclusions) {
|
||||
AyuDatabase::deleteExclusion(exclusion.dialogId, exclusion.filterId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.peersToBeResolved.empty()) {
|
||||
resolveAllChats(changes.peersToBeResolved);
|
||||
}
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
crl::on_main([]
|
||||
{
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
#include "ayu/data/entities.h"
|
||||
#include "core/application.h"
|
||||
|
||||
#include "data/data_session.h"
|
||||
#include "history/history_item_components.h"
|
||||
|
||||
struct ApplyChanges
|
||||
{
|
||||
std::vector<RegexFilter> newFilters;
|
||||
std::vector<QString> removeFiltersById;
|
||||
|
||||
std::vector<RegexFilter> filtersOverrides;
|
||||
|
||||
std::vector<RegexFilterGlobalExclusion> newExclusions;
|
||||
std::vector<RegexFilterGlobalExclusion> removeExclusions;
|
||||
|
||||
std::map<long long, QString> peersToBeResolved;
|
||||
|
||||
auto operator<=>(const ApplyChanges&) const = default;
|
||||
};
|
||||
|
||||
|
||||
class FilterUtils final: public QObject
|
||||
{
|
||||
|
||||
|
||||
Q_OBJECT
|
||||
public:
|
||||
static FilterUtils &getInstance() {
|
||||
static FilterUtils instance;
|
||||
return instance;
|
||||
}
|
||||
FilterUtils(const FilterUtils &) = delete;
|
||||
FilterUtils &operator=(const FilterUtils &) = delete;
|
||||
FilterUtils(FilterUtils &&) = delete;
|
||||
FilterUtils &operator=(FilterUtils &&) = delete;
|
||||
|
||||
|
||||
void importFromLink(const QString &link);
|
||||
bool importFromJson(const QByteArray &json);
|
||||
|
||||
void publishFilters();
|
||||
static QString exportFilters();
|
||||
|
||||
static QString extractAllText(not_null<HistoryItem *> item);
|
||||
private:
|
||||
FilterUtils():
|
||||
_manager(std::make_unique<QNetworkAccessManager>()) {
|
||||
|
||||
}
|
||||
|
||||
bool handleResponse(const QByteArray &response);
|
||||
void gotFailure(const QNetworkReply::NetworkError &error);
|
||||
|
||||
ApplyChanges prepareChanges(const QJsonObject &response);
|
||||
void applyChanges(const ApplyChanges &changes);
|
||||
|
||||
QTimer* _timer = nullptr;
|
||||
|
||||
std::unique_ptr<QNetworkAccessManager> _manager = nullptr;
|
||||
QNetworkReply *_reply = nullptr;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "shadow_ban_utils.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
#include "filters_cache_controller.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/data/entities.h"
|
||||
|
||||
void ShadowBanUtils::addShadowBan(ID userId) {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
if (settings.shadowBanIds.insert(userId).second) {
|
||||
setShadowBanList();
|
||||
}
|
||||
}
|
||||
|
||||
void ShadowBanUtils::removeShadowBan(ID userId) {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
if (settings.shadowBanIds.erase(userId) > 0) {
|
||||
setShadowBanList();
|
||||
}
|
||||
}
|
||||
|
||||
bool ShadowBanUtils::isShadowBanned(ID userId) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
return settings.shadowBanIds.contains(userId);
|
||||
}
|
||||
|
||||
void ShadowBanUtils::setShadowBanList() {
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
AyuSettings::save();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include <unordered_set>
|
||||
|
||||
class ShadowBanUtils {
|
||||
public:
|
||||
static void addShadowBan(long long userId);
|
||||
static void removeShadowBan(long long userId);
|
||||
static bool isShadowBanned(long long userId);
|
||||
|
||||
private:
|
||||
ShadowBanUtils() = delete;
|
||||
|
||||
static void setShadowBanList();
|
||||
};
|
||||
@@ -33,7 +33,7 @@ namespace {
|
||||
const QByteArray &payload,
|
||||
const QString &iconPath,
|
||||
int pixel,
|
||||
int max) {
|
||||
int max) {
|
||||
Expects(!payload.isEmpty());
|
||||
|
||||
const auto data = Qr::Encode(payload, Qr::Redundancy::Default);
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "ayu/ui/boxes/import_filters_box.h"
|
||||
|
||||
#include "qr/qr_generate.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_intro.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/rp_widget.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/controls/invite_link_label.h"
|
||||
#include "ui/layers/generic_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtSvg/QSvgRenderer>
|
||||
|
||||
#include "lang_auto.h"
|
||||
#include "ayu/features/filters/filters_utils.h"
|
||||
#include "styles/style_giveaway.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
|
||||
namespace Ui {
|
||||
|
||||
void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import) {
|
||||
box->setStyle(st::giveawayGiftCodeBox);
|
||||
box->setNoContentMargin(true);
|
||||
box->setTitle(import ? tr::ayu_FiltersMenuImport() : tr::ayu_FiltersMenuExport());
|
||||
box->verticalLayout()->resizeToWidth(box->width());
|
||||
|
||||
const auto container = box->verticalLayout();
|
||||
|
||||
const auto skip = st::settingsSendTypeSkip;
|
||||
auto wrap = object_ptr<Ui::VerticalLayout>(container);
|
||||
const auto inner = wrap.data();
|
||||
container->add(
|
||||
object_ptr<Ui::OverrideMargins>(
|
||||
container,
|
||||
std::move(wrap),
|
||||
QMargins(0, skip, 0, skip)));
|
||||
|
||||
Ui::InputField *importURLField = nullptr;
|
||||
Ui::SlideWrap<Ui::VerticalLayout> *importURLWrap = nullptr;
|
||||
|
||||
const auto intoURL = std::make_shared<RadioenumGroup<bool>>(false);
|
||||
const auto addOption = [&](bool value, const QString &text)
|
||||
{
|
||||
inner->add(
|
||||
object_ptr<Ui::Radioenum<bool>>(
|
||||
inner,
|
||||
intoURL,
|
||||
value,
|
||||
text,
|
||||
st::settingsSendType),
|
||||
st::settingsSendTypePadding);
|
||||
|
||||
if (import && value) {
|
||||
const auto clipboardText = QGuiApplication::clipboard()->text().trimmed();
|
||||
const auto prefill = clipboardText.startsWith("http") ? clipboardText : QString();
|
||||
|
||||
importURLWrap = inner->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||
inner,
|
||||
object_ptr<Ui::VerticalLayout>(inner),
|
||||
st::giveawayGiftCodeBox.buttonPadding
|
||||
)
|
||||
);
|
||||
importURLField = importURLWrap->entity()->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
container,
|
||||
st::defaultInputField,
|
||||
rpl::single(QString("URL")),
|
||||
prefill
|
||||
)
|
||||
);
|
||||
importURLWrap->hide(anim::type::instant);
|
||||
}
|
||||
};
|
||||
addOption(false, import ? tr::ayu_FiltersImportClipboard(tr::now) : tr::ayu_FiltersExportClipboard(tr::now));
|
||||
addOption(true, import ? tr::ayu_FiltersImportURL(tr::now) : tr::ayu_FiltersExportURL(tr::now));
|
||||
|
||||
intoURL->setChangedCallback([=](bool value)
|
||||
{
|
||||
if (import) {
|
||||
importURLWrap->toggle(value, anim::type::normal);
|
||||
}
|
||||
});
|
||||
|
||||
const auto actionButton = box->addButton(
|
||||
import ? tr::ayu_FiltersMenuImport() : tr::ayu_FiltersMenuExport(),
|
||||
[=]
|
||||
{
|
||||
const auto isURL = intoURL.get()->current();
|
||||
|
||||
if (import) {
|
||||
if (isURL) {
|
||||
FilterUtils::getInstance().importFromLink(
|
||||
importURLField->getLastText().trimmed());
|
||||
} else {
|
||||
FilterUtils::getInstance().importFromJson(
|
||||
QGuiApplication::clipboard()->text().toUtf8());
|
||||
}
|
||||
} else {
|
||||
if (isURL) {
|
||||
FilterUtils::getInstance().publishFilters();
|
||||
} else {
|
||||
const auto data = FilterUtils::getInstance().exportFilters();
|
||||
QGuiApplication::clipboard()->setText(data);
|
||||
|
||||
Toast::Show(tr::lng_text_copied(tr::now));
|
||||
}
|
||||
}
|
||||
box->closeBox();
|
||||
});
|
||||
const auto buttonWidth = box->width()
|
||||
- rect::m::sum::h(st::giveawayGiftCodeBox.buttonPadding);
|
||||
actionButton->widthValue() | rpl::filter([=]
|
||||
{
|
||||
return (actionButton->widthNoMargins() != buttonWidth);
|
||||
}) | rpl::start_with_next([=]
|
||||
{
|
||||
actionButton->resizeToWidth(buttonWidth);
|
||||
},
|
||||
actionButton->lifetime());
|
||||
|
||||
box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||
}
|
||||
|
||||
} // namespace Ui
|
||||
@@ -0,0 +1,15 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
namespace Ui {
|
||||
|
||||
class GenericBox;
|
||||
|
||||
void FillImportFiltersBox(not_null<Ui::GenericBox*> box, bool import);
|
||||
|
||||
} // namespace Ui
|
||||
@@ -9,10 +9,10 @@
|
||||
#include "apiwrap.h"
|
||||
#include "lang_auto.h"
|
||||
#include "mainwidget.h"
|
||||
#include "mainwindow.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/ayu_state.h"
|
||||
#include "ayu/data/messages_storage.h"
|
||||
#include "ayu/features/filters/shadow_ban_utils.h"
|
||||
#include "ayu/ui/context_menu/menu_item_subtext.h"
|
||||
#include "ayu/utils/qt_key_modifiers_extended.h"
|
||||
#include "history/history_item_components.h"
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "window/window_peer_menu.h"
|
||||
|
||||
#include "ayu/ui/message_history/history_section.h"
|
||||
#include "ayu/ui/settings/filters/edit_filter.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "base/call_delayed.h"
|
||||
#include "base/random.h"
|
||||
@@ -210,6 +211,7 @@ void AddDeletedMessagesActions(PeerData *peerData,
|
||||
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData, nullptr, topicId));
|
||||
},
|
||||
&st::menuIconArchive);
|
||||
// todo view filters
|
||||
}
|
||||
|
||||
void AddJumpToBeginningAction(PeerData *peerData,
|
||||
@@ -313,6 +315,33 @@ void AddOpenChannelAction(PeerData *peerData,
|
||||
&st::menuIconChannel);
|
||||
}
|
||||
|
||||
void AddShadowBanAction(PeerData *peerData,
|
||||
const Window::PeerMenuCallback &addCallback) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
if (!peerData || !peerData->isUser() || !settings.filtersEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto realId = getDialogIdFromPeer(peerData);
|
||||
const auto shadowBanned = ShadowBanUtils::isShadowBanned(realId);
|
||||
const auto toggleShadowBan = [=]
|
||||
{
|
||||
if (shadowBanned) {
|
||||
ShadowBanUtils::removeShadowBan(realId);
|
||||
} else {
|
||||
ShadowBanUtils::addShadowBan(realId);
|
||||
}
|
||||
};
|
||||
|
||||
addCallback({
|
||||
.text = (shadowBanned
|
||||
? tr::ayu_FiltersQuickUnshadowBan(tr::now)
|
||||
: tr::ayu_FiltersQuickShadowBan(tr::now)),
|
||||
.handler = toggleShadowBan,
|
||||
.icon = shadowBanned ? &st::menuIconShowInChat : &st::menuIconStealth,
|
||||
});
|
||||
}
|
||||
|
||||
void AddDeleteOwnMessagesAction(PeerData *peerData,
|
||||
Data::ForumTopic *topic,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
@@ -718,4 +747,30 @@ void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
|
||||
&st::menuIconTTLAny);
|
||||
}
|
||||
|
||||
void AddCreateFilterAction(not_null<Ui::PopupMenu*> menu,
|
||||
not_null<Window::SessionController*> controller,
|
||||
HistoryItem *item,
|
||||
const QString &selectedText) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
if (!needToShowItem(settings.showAddFilterInContextMenu) || !settings.filtersEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item || selectedText.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu->addAction(
|
||||
tr::ayu_RegexFilterQuickAdd(tr::now),
|
||||
[=]
|
||||
{
|
||||
RegexFilter filter;
|
||||
filter.text = selectedText.toStdString();
|
||||
filter.reversed = false;
|
||||
|
||||
controller->show(Settings::RegexEditBox(&filter, {}, getDialogIdFromPeer(item->history()->peer), true));
|
||||
},
|
||||
&st::menuIconAddToFolder);
|
||||
}
|
||||
|
||||
} // namespace AyuUi
|
||||
|
||||
@@ -25,11 +25,13 @@ void AddJumpToBeginningAction(PeerData *peerData,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
|
||||
void AddShadowBanAction(PeerData *peerData,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
void AddOpenChannelAction(PeerData *peerData,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
void AddDeleteOwnMessagesAction(PeerData *peerData,
|
||||
Data::ForumTopic* topic,
|
||||
Data::ForumTopic *topic,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
const Window::PeerMenuCallback &addCallback);
|
||||
|
||||
@@ -39,5 +41,9 @@ void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddReadUntilAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddBurnAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
|
||||
void AddCreateFilterAction(not_null<Ui::PopupMenu*> menu,
|
||||
not_null<Window::SessionController*> controller,
|
||||
HistoryItem *item,
|
||||
const QString &selectedText);
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
using "ui/basic.style";
|
||||
using "ui/layers/layers.style";
|
||||
using "ui/widgets/widgets.style";
|
||||
using "info/info.style";
|
||||
|
||||
centeredBoxLabelStyle: TextStyle(boxLabelStyle) {
|
||||
font: font(13px);
|
||||
@@ -21,3 +22,17 @@ centeredBoxLabel: FlatLabel(defaultFlatLabel) {
|
||||
align: align(center);
|
||||
style: centeredBoxLabelStyle;
|
||||
}
|
||||
|
||||
filtersAddIcon: IconButton(infoLayerTopBarClose) {
|
||||
width: 40px;
|
||||
icon: icon {{ "menu/add", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/add", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
||||
|
||||
filtersExcludeIcon: IconButton(infoLayerTopBarClose) {
|
||||
width: 40px;
|
||||
icon: icon {{ "menu/tag_remove", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/tag_remove", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "edit_filter.h"
|
||||
|
||||
#include "lang_auto.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "boxes/delete_messages_box.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_layers.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_window.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
|
||||
|
||||
namespace Settings {
|
||||
|
||||
std::vector<char> generate_uuid_bytes() {
|
||||
// stolen somewhere from Internet
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<uint32_t> dist;
|
||||
|
||||
std::vector<uint8_t> bytes(16);
|
||||
for (int i = 0; i < 16; i += 4) {
|
||||
uint32_t random_chunk = dist(gen);
|
||||
bytes[i] = random_chunk & 0xFF;
|
||||
bytes[i + 1] = (random_chunk >> 8) & 0xFF;
|
||||
bytes[i + 2] = (random_chunk >> 16) & 0xFF;
|
||||
bytes[i + 3] = (random_chunk >> 24) & 0xFF;
|
||||
}
|
||||
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
||||
|
||||
return std::vector<char>(bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
bool validateRegex(const icu::UnicodeString& pattern, std::string& errorMsg) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
UParseError parseError;
|
||||
|
||||
icu::RegexPattern* regexPattern = icu::RegexPattern::compile(
|
||||
pattern,
|
||||
0, // flags
|
||||
parseError,
|
||||
status
|
||||
);
|
||||
|
||||
if (U_FAILURE(status)) {
|
||||
auto errorCodeNormalized = std::string(u_errorName(status));
|
||||
errorCodeNormalized = errorCodeNormalized.substr(8); // skip U_REGEX_
|
||||
std::ranges::transform(
|
||||
errorCodeNormalized,
|
||||
errorCodeNormalized.begin(),
|
||||
[](unsigned char c)
|
||||
{
|
||||
if (c == '_') {
|
||||
return std::tolower(' ');
|
||||
}
|
||||
return std::tolower(c);
|
||||
});
|
||||
errorCodeNormalized[0] = std::toupper(errorCodeNormalized[0]);
|
||||
errorMsg = errorCodeNormalized + " at " + std::to_string(parseError.offset);
|
||||
|
||||
if (parseError.preContext[0] != 0 || parseError.postContext[0] != 0) {
|
||||
icu::UnicodeString pre(parseError.preContext);
|
||||
icu::UnicodeString post(parseError.postContext);
|
||||
std::string preStr, postStr;
|
||||
pre.toUTF8String(preStr);
|
||||
post.toUTF8String(postStr);
|
||||
errorMsg += " (near: '" + preStr + "' -> '" + postStr + "')";
|
||||
}
|
||||
|
||||
delete regexPattern;
|
||||
return false;
|
||||
}
|
||||
|
||||
delete regexPattern;
|
||||
return true;
|
||||
}
|
||||
|
||||
not_null<Ui::SlideWrap<Ui::FlatLabel>*> AddError(
|
||||
not_null<Ui::VerticalLayout*> content,
|
||||
Ui::InputField *input) {
|
||||
|
||||
std::string errorText;
|
||||
validateRegex(icu::UnicodeString::fromUTF8("("), errorText);
|
||||
|
||||
const auto error = content->add(
|
||||
object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
|
||||
content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
QString::fromStdString(errorText) + QString::fromStdString(errorText),
|
||||
st::settingLocalPasscodeError), st::settingsCheckboxPadding));
|
||||
error->hide(anim::type::instant);
|
||||
if (input) {
|
||||
input->changes() | rpl::start_with_next(
|
||||
[=]
|
||||
{
|
||||
error->hide(anim::type::normal);
|
||||
},
|
||||
input->lifetime());
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
void RegexEditBuilder(
|
||||
not_null<Ui::GenericBox*> box,
|
||||
RegexFilter *filter,
|
||||
const Fn<void(RegexFilter)> &onDone,
|
||||
std::optional<long long> dialogId,
|
||||
bool showToast
|
||||
) {
|
||||
RegexFilter data;
|
||||
|
||||
if (filter) {
|
||||
box->setTitle(tr::ayu_RegexFiltersEdit());
|
||||
data = *filter;
|
||||
} else {
|
||||
box->setTitle(tr::ayu_RegexFiltersAdd());
|
||||
data.reversed = false;
|
||||
}
|
||||
|
||||
const auto regexValue = box->addRow(
|
||||
object_ptr<Ui::InputField>(
|
||||
box->verticalLayout(),
|
||||
st::windowFilterNameInput,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::ayu_RegexFiltersPlaceholder()),
|
||||
st::markdownLinkFieldPadding);
|
||||
const auto errorText = AddError(box->verticalLayout(), regexValue);
|
||||
const auto enabled = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::ayu_EnableExpression(tr::now),
|
||||
data.enabled,
|
||||
st::defaultBoxCheckbox),
|
||||
st::settingsCheckboxPadding);
|
||||
const auto caseInsensitive = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::ayu_CaseInsensitiveExpression(tr::now),
|
||||
data.caseInsensitive,
|
||||
st::defaultBoxCheckbox),
|
||||
st::settingsCheckboxPadding);
|
||||
const auto reversed = box->addRow(
|
||||
object_ptr<Ui::Checkbox>(
|
||||
box,
|
||||
tr::ayu_ReversedExpression(tr::now),
|
||||
data.reversed,
|
||||
st::defaultBoxCheckbox),
|
||||
st::settingsCheckboxPadding);
|
||||
|
||||
regexValue->setText(QString::fromStdString(data.text));
|
||||
|
||||
auto saveAndClose = [=, id = data.id]
|
||||
{
|
||||
const auto text = regexValue->getTextWithTags().text;
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error;
|
||||
if (!validateRegex(icu::UnicodeString::fromUTF8(text.toStdString()), error)) {
|
||||
errorText->entity()->setText(QString::fromStdString(error));
|
||||
errorText->show(anim::type::normal);
|
||||
return;
|
||||
}
|
||||
|
||||
RegexFilter newFilter;
|
||||
newFilter.text = regexValue->getTextWithTags().text.toStdString();
|
||||
newFilter.enabled = enabled->checked();
|
||||
newFilter.caseInsensitive = caseInsensitive->checked();
|
||||
newFilter.reversed = reversed->checked();
|
||||
|
||||
if (!showToast && dialogId.has_value()) {
|
||||
newFilter.dialogId = dialogId;
|
||||
}
|
||||
|
||||
if (!id.empty()) {
|
||||
newFilter.id = id;
|
||||
} else {
|
||||
newFilter.id = generate_uuid_bytes();
|
||||
}
|
||||
|
||||
box->closeBox();
|
||||
|
||||
crl::async([=]
|
||||
{
|
||||
AyuDatabase::addRegexFilter(newFilter);
|
||||
FiltersCacheController::rebuildCache();
|
||||
|
||||
crl::on_main([=]
|
||||
{
|
||||
if (onDone) {
|
||||
onDone(newFilter);
|
||||
}
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
|
||||
if (showToast) {
|
||||
const auto onClick = [=](const auto &...) mutable
|
||||
{
|
||||
newFilter.dialogId = dialogId;
|
||||
|
||||
AyuDatabase::updateRegexFilter(newFilter);
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
|
||||
return true;
|
||||
};
|
||||
// todo: custom toast with "Move to shared" button
|
||||
// based on `PaidReactionToast`
|
||||
Ui::Toast::Show(Ui::Toast::Config{
|
||||
.text = tr::ayu_RegexFilterBulletinText(
|
||||
tr::now,
|
||||
Ui::Text::RichLangValue
|
||||
),
|
||||
.filter = onClick,
|
||||
.adaptive = true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
regexValue->submits() | rpl::start_with_next(saveAndClose, regexValue->lifetime());
|
||||
box->addButton(tr::lng_settings_save(), saveAndClose);
|
||||
box->addButton(tr::lng_cancel(),
|
||||
[=]
|
||||
{
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
errorText->entity()->resizeToWidth(box->width());
|
||||
errorText->resizeToWidth(box->width());
|
||||
}
|
||||
|
||||
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter *filter,
|
||||
const Fn<void(RegexFilter)> &onDone,
|
||||
std::optional<long long> dialogId,
|
||||
bool showToast) {
|
||||
return Box(RegexEditBuilder, filter, onDone, dialogId, showToast);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
@@ -0,0 +1,28 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "ayu/data/entities.h"
|
||||
#include "settings/settings_common_session.h"
|
||||
|
||||
#include "boxes/premium_limits_box.h"
|
||||
#include "base/unixtime.h"
|
||||
|
||||
class BoxContent;
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Settings {
|
||||
|
||||
object_ptr<Ui::GenericBox> RegexEditBox(RegexFilter *filter,
|
||||
const Fn<void(RegexFilter)> &onDone,
|
||||
std::optional<long long> dialogId = std::nullopt,
|
||||
bool showToast = false);
|
||||
} // namespace Settings
|
||||
@@ -0,0 +1,161 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "per_dialog_filter.h"
|
||||
|
||||
#include <utility>
|
||||
#include "lang_auto.h"
|
||||
#include "settings_filters_list.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "data/data_session.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/painter.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
PerDialogFiltersListRow::PerDialogFiltersListRow(PeerId peer)
|
||||
: PeerListRow(peer.value)
|
||||
, peerId(peer) {
|
||||
}
|
||||
|
||||
QString PerDialogFiltersListRow::generateName() {
|
||||
if (const auto from = getPeerFromDialogId(peerId.value & PeerId::kChatTypeMask)) {
|
||||
this->setPeer(from);
|
||||
return PeerListRow::generateName();
|
||||
}
|
||||
return QString("UNKNOWN (ID: %1)").arg(QString::number(peerId.value & PeerId::kChatTypeMask));
|
||||
}
|
||||
|
||||
PaintRoundImageCallback PerDialogFiltersListRow::generatePaintUserpicCallback(bool forceRound) {
|
||||
if (const auto from = getPeerFromDialogId(peerId.value & PeerId::kChatTypeMask)) {
|
||||
this->setPeer(from);
|
||||
return PeerListRow::generatePaintUserpicCallback(forceRound);
|
||||
}
|
||||
|
||||
return [=](Painter &p, int x, int y, int outerWidth, int size) mutable
|
||||
{
|
||||
using namespace Ui;
|
||||
const auto realId = peerId.value & PeerId::kChatTypeMask;
|
||||
auto _userpicEmpty = std::make_unique<EmptyUserpic>(
|
||||
EmptyUserpic::UserpicColor(realId % 7),
|
||||
QString("U")); // U - Unknown
|
||||
_userpicEmpty->paintCircle(p, x, y, outerWidth, size);
|
||||
};
|
||||
}
|
||||
|
||||
PerDialogFiltersListController::PerDialogFiltersListController(not_null<Main::Session*> session,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: _session(session)
|
||||
, _controller(controller) {
|
||||
}
|
||||
|
||||
Main::Session &PerDialogFiltersListController::session() const {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
void PerDialogFiltersListController::prepare() {
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
if (filters.empty() && exclusions.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
countsByDialogIds.clear();
|
||||
|
||||
for (const auto &filter : filters) {
|
||||
if (filter.dialogId.has_value()) {
|
||||
countsByDialogIds[filter.dialogId.value()].filters++;
|
||||
}
|
||||
}
|
||||
for (const auto &exclusion : exclusions) {
|
||||
countsByDialogIds[exclusion.dialogId].exclusions++;
|
||||
}
|
||||
|
||||
for (const auto &[id, count] : countsByDialogIds) {
|
||||
PeerId peerId = PeerId(PeerIdHelper(abs(id)));
|
||||
|
||||
auto row = std::make_unique<PerDialogFiltersListRow>(peerId);
|
||||
auto status = QString();
|
||||
if (count.filters > 0) {
|
||||
status += tr::ayu_RegexFiltersAmount(tr::now, lt_count, count.filters);
|
||||
if (count.exclusions > 0) {
|
||||
status += ", ";
|
||||
}
|
||||
}
|
||||
if (count.exclusions > 0) {
|
||||
status += tr::ayu_RegexFiltersExcludedAmount(tr::now, lt_count, count.exclusions);
|
||||
}
|
||||
|
||||
row->setCustomStatus(status, false);
|
||||
|
||||
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow>&&>(row));
|
||||
}
|
||||
|
||||
// sortByName();
|
||||
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void PerDialogFiltersListController::rowClicked(not_null<PeerListRow*> peer) {
|
||||
ID did;
|
||||
if (peer->special()) {
|
||||
const ID pred = peer->id() & PeerId::kChatTypeMask;
|
||||
if (countsByDialogIds.contains(pred)) {
|
||||
did = pred;
|
||||
} else {
|
||||
did = -pred;
|
||||
}
|
||||
} else {
|
||||
did = getDialogIdFromPeer(peer->peer());
|
||||
}
|
||||
_controller->dialogId = did;
|
||||
_controller->showExclude = true;
|
||||
_controller->showSettings(AyuFiltersList::Id());
|
||||
}
|
||||
|
||||
SelectChatBoxController::SelectChatBoxController(
|
||||
not_null<Window::SessionController*> controller,
|
||||
Fn<void(not_null<PeerData*>)> onSelectedCallback)
|
||||
: ChatsListBoxController(&controller->session())
|
||||
, _controller(controller)
|
||||
, _onSelectedCallback(std::move(onSelectedCallback)) {
|
||||
}
|
||||
|
||||
Main::Session &SelectChatBoxController::session() const {
|
||||
return _controller->session();
|
||||
}
|
||||
|
||||
void SelectChatBoxController::rowClicked(not_null<PeerListRow*> row) {
|
||||
if (_onSelectedCallback) {
|
||||
_onSelectedCallback(row->peer());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ChatsListBoxController::Row> SelectChatBoxController::createRow(not_null<History*> history) {
|
||||
const auto peer = history->peer;
|
||||
|
||||
const auto skip =
|
||||
peer->isUser() ||
|
||||
//peer->forum() ||
|
||||
peer->monoforum();
|
||||
|
||||
if (skip) {
|
||||
return nullptr;
|
||||
}
|
||||
auto result = std::make_unique<Row>(
|
||||
history,
|
||||
nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
void SelectChatBoxController::prepareViewHook() {
|
||||
delegate()->peerListSetTitle(tr::ayu_FiltersMenuSelectChat());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "ayu/data/entities.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "boxes/peer_list_controllers.h"
|
||||
#include "data/data_peer.h"
|
||||
#include "history/history.h"
|
||||
|
||||
class RegexFilterGlobalExclusion;
|
||||
|
||||
namespace Main {
|
||||
class Session;
|
||||
} // namespace Main
|
||||
|
||||
namespace Settings {
|
||||
|
||||
class PerDialogFiltersListRow final : public PeerListRow
|
||||
{
|
||||
public:
|
||||
explicit PerDialogFiltersListRow(PeerId peer);
|
||||
QString generateName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(bool forceRound) override;
|
||||
|
||||
private:
|
||||
PeerId peerId;
|
||||
};
|
||||
|
||||
class PerDialogFiltersListController final : public PeerListController
|
||||
{
|
||||
public:
|
||||
explicit PerDialogFiltersListController(not_null<Main::Session*> session,
|
||||
not_null<Window::SessionController*> controller);
|
||||
|
||||
[[nodiscard]] Main::Session &session() const override;
|
||||
|
||||
void prepare() override;
|
||||
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
private:
|
||||
struct FilterCounts
|
||||
{
|
||||
int filters = 0;
|
||||
int exclusions = 0;
|
||||
};
|
||||
std::unordered_map<ID, FilterCounts> countsByDialogIds;
|
||||
|
||||
const not_null<Main::Session*> _session;
|
||||
not_null<Window::SessionController*> _controller;
|
||||
};
|
||||
|
||||
class SelectChatBoxController
|
||||
: public ChatsListBoxController
|
||||
, public base::has_weak_ptr
|
||||
{
|
||||
public:
|
||||
explicit SelectChatBoxController(
|
||||
not_null<Window::SessionController*> controller,
|
||||
Fn<void(not_null<PeerData*>)> onSelectedCallback
|
||||
);
|
||||
|
||||
Main::Session &session() const override;
|
||||
void rowClicked(not_null<PeerListRow*> row) override;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Row> createRow(not_null<History*> history) override;
|
||||
void prepareViewHook() override;
|
||||
|
||||
private:
|
||||
not_null<Window::SessionController*> _controller;
|
||||
Fn<void(not_null<PeerData*>)> _onSelectedCallback;
|
||||
};
|
||||
} // namespace Settings
|
||||
@@ -0,0 +1,269 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "settings_filters_list.h"
|
||||
|
||||
#include <styles/style_layers.h>
|
||||
#include <styles/style_media_view.h>
|
||||
|
||||
#include "edit_filter.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
|
||||
#include "lang_auto.h"
|
||||
|
||||
#include "boxes/connection_box.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "storage/localstorage.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
#include "../../components/icon_picker.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "ayu/features/filters/filters_utils.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "info/info_wrap_widget.h"
|
||||
#include "ui/qt_object_factory.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
rpl::producer<QString> AyuFiltersList::title() {
|
||||
if (!dialogId.has_value()) {
|
||||
return tr::ayu_RegexFiltersShared();
|
||||
}
|
||||
|
||||
const auto did = abs(dialogId.value());
|
||||
const auto from = getPeerFromDialogId(did);
|
||||
|
||||
// todo: shorten based on available space
|
||||
// because it may break on custom fonts
|
||||
QString res;
|
||||
if (from) {
|
||||
auto name = from->topBarNameText();
|
||||
if (name.length() > 18) {
|
||||
name = name.left(17) + "…";
|
||||
}
|
||||
res = name;
|
||||
} else {
|
||||
res = tr::ayu_RegexFiltersHeader(tr::now) + " (" + QString::number(did) + ")";
|
||||
}
|
||||
|
||||
return rpl::single(res);
|
||||
}
|
||||
|
||||
AyuFiltersList::AyuFiltersList(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: Section(parent), _controller(controller), _content(Ui::CreateChild<Ui::VerticalLayout>(this)) {
|
||||
if (_controller->dialogId.has_value()) {
|
||||
dialogId = _controller->dialogId.value();
|
||||
}
|
||||
|
||||
setupContent(controller);
|
||||
}
|
||||
|
||||
void AyuFiltersList::checkBeforeClose(Fn<void()> close) {
|
||||
_controller->showExclude = true;
|
||||
close();
|
||||
}
|
||||
|
||||
void AyuFiltersList::addNewFilter(const RegexFilter &filter, bool exclusion) {
|
||||
const auto state = lifetime().make_state<RegexFilter>(filter);
|
||||
const auto button = _content->add(
|
||||
object_ptr<Button>(
|
||||
_content,
|
||||
rpl::single(QString::fromStdString(state->text).replace("\n", " ")),
|
||||
st::settingsButtonNoIcon
|
||||
)
|
||||
);
|
||||
|
||||
if (!state->enabled) {
|
||||
button->setColorOverride(st::storiesComposeGrayText->c);
|
||||
}
|
||||
|
||||
auto defaultClickHandler = [=, this]() mutable
|
||||
{
|
||||
auto _contextMenu = new Ui::PopupMenu(this, st::popupMenuWithIcons);
|
||||
_contextMenu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
_contextMenu->addAction(
|
||||
tr::lng_theme_edit(tr::now),
|
||||
[=, this]
|
||||
{
|
||||
_controller->show(
|
||||
RegexEditBox(
|
||||
state,
|
||||
nullptr
|
||||
));
|
||||
},
|
||||
&st::menuIconEdit);
|
||||
|
||||
_contextMenu->addAction(
|
||||
state->enabled ? tr::lng_settings_auto_night_disable(tr::now) : tr::lng_sure_enable(tr::now),
|
||||
[=]
|
||||
{
|
||||
state->enabled = !state->enabled;
|
||||
AyuDatabase::updateRegexFilter(*state);
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
state->enabled ? &st::menuIconBlock : &st::menuIconUnblock);
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
_contextMenu->addAction(
|
||||
tr::lng_theme_delete(tr::now),
|
||||
[=]
|
||||
{
|
||||
AyuDatabase::deleteFilter(state->id);
|
||||
AyuDatabase::deleteExclusionsByFilterId(state->id);
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
&st::menuIconDelete);
|
||||
|
||||
_contextMenu->popup(QCursor::pos());
|
||||
};
|
||||
|
||||
// we've opened filters list from top "Exclude" button
|
||||
// on click, close the section
|
||||
auto exclusionsClickHandler = [=, controller = _controller, dialogId = dialogId]() mutable
|
||||
{
|
||||
Expects(dialogId.has_value());
|
||||
|
||||
/*
|
||||
└── class Info::WrapWidget
|
||||
└── class Info::Settings::Widget
|
||||
└── class Ui::ScrollArea
|
||||
└── class QWidget
|
||||
└── class Ui::PaddingWrap<class Ui::RpWidget>
|
||||
└── class Settings::AyuFiltersList
|
||||
*/
|
||||
// controller->showBackFromStack() doesn't work (closes box completely)
|
||||
// so as a workaround, use WrapWidget
|
||||
const auto wrap = dynamic_cast<Info::WrapWidget*>(parent()->parent()->parent()->parent()->parent());
|
||||
|
||||
const RegexFilterGlobalExclusion newExclusion = {
|
||||
.dialogId = dialogId.value(),
|
||||
.filterId = state->id
|
||||
};
|
||||
|
||||
AyuDatabase::addRegexExclusion(newExclusion);
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
|
||||
controller->dialogId = dialogId;
|
||||
controller->showExclude = true;
|
||||
|
||||
wrap->showBackFromStackInternal(Window::SectionShow(anim::type::normal));
|
||||
};
|
||||
auto deleteExclusionsClickHandler = [=, this]() mutable
|
||||
{
|
||||
auto _contextMenu = new Ui::PopupMenu(this, st::popupMenuWithIcons);
|
||||
_contextMenu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
_contextMenu->addAction(
|
||||
tr::lng_theme_delete(tr::now),
|
||||
[=, this]
|
||||
{
|
||||
Expects(dialogId.has_value());
|
||||
|
||||
AyuDatabase::deleteExclusion(dialogId.value(), state->id);
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
&st::menuIconDelete);
|
||||
|
||||
_contextMenu->popup(QCursor::pos());
|
||||
};
|
||||
|
||||
if (exclusion) {
|
||||
button->addClickHandler(deleteExclusionsClickHandler);
|
||||
} else if (dialogId.has_value() && _controller->showExclude.has_value() && !_controller->showExclude.value()) {
|
||||
button->addClickHandler(exclusionsClickHandler);
|
||||
} else {
|
||||
button->addClickHandler(defaultClickHandler);
|
||||
}
|
||||
|
||||
|
||||
crl::on_main(
|
||||
this,
|
||||
[=, this]
|
||||
{
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
});
|
||||
}
|
||||
|
||||
void AyuFiltersList::initializeSharedFilters(
|
||||
not_null<Ui::VerticalLayout*> container) {
|
||||
if (dialogId.has_value() && _controller->showExclude.has_value() && _controller->showExclude.value()) {
|
||||
filters = AyuDatabase::getByDialogId(dialogId.value());
|
||||
exclusions = AyuDatabase::getExcludedByDialogId(dialogId.value());
|
||||
} else {
|
||||
filters = AyuDatabase::getShared();
|
||||
|
||||
// remove shared filters that already excluded for that peer exclusion
|
||||
if (dialogId.has_value() && _controller->showExclude.has_value() && !_controller->showExclude.value()) {
|
||||
const auto excludedForDialogId = AyuDatabase::getExcludedByDialogId(dialogId.value());
|
||||
|
||||
auto rangeToRemove = std::ranges::remove_if(
|
||||
filters,
|
||||
[&](const RegexFilter &filter)
|
||||
{
|
||||
for (const auto &excluded : excludedForDialogId) {
|
||||
if (excluded == filter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
filters.erase(rangeToRemove.begin(), rangeToRemove.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (!filters.empty()) {
|
||||
AddSkip(container);
|
||||
filtersTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersHeader());
|
||||
|
||||
for (const auto &filter : filters) {
|
||||
addNewFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exclusions.empty()) {
|
||||
if (!filters.empty()) {
|
||||
AddSkip(container);
|
||||
AddDivider(container);
|
||||
AddSkip(container);
|
||||
}
|
||||
|
||||
excludedTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersExcluded());
|
||||
|
||||
for (const auto &exclusion : exclusions) {
|
||||
addNewFilter(exclusion, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.empty() && exclusions.empty()) {
|
||||
Ui::AddDividerText(container, tr::ayu_RegexFiltersListEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
void AyuFiltersList::setupContent(not_null<Window::SessionController*> controller) {
|
||||
initializeSharedFilters(_content);
|
||||
|
||||
ResizeFitChild(this, _content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
@@ -0,0 +1,51 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "ayu/data/entities.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "settings/settings_common_session.h"
|
||||
#include "ui/layers/box_content.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
|
||||
class BoxContent;
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Settings {
|
||||
|
||||
class AyuFiltersList : public Section<AyuFiltersList>
|
||||
{
|
||||
public:
|
||||
AyuFiltersList(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> title() override;
|
||||
|
||||
void checkBeforeClose(Fn<void()> close) override;
|
||||
|
||||
private:
|
||||
void setupContent(not_null<Window::SessionController*> controller);
|
||||
void initializeSharedFilters(not_null<Ui::VerticalLayout*> container);
|
||||
|
||||
void addNewFilter(const RegexFilter &filter, bool exclusion = false);
|
||||
|
||||
not_null<Window::SessionController*> _controller;
|
||||
not_null<Ui::VerticalLayout*> _content;
|
||||
|
||||
std::vector<RegexFilter> filters;
|
||||
std::vector<RegexFilter> exclusions;
|
||||
|
||||
Ui::FlatLabel *filtersTitle = nullptr;
|
||||
Ui::FlatLabel *excludedTitle = nullptr;
|
||||
|
||||
std::optional<long long> dialogId;
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
@@ -9,6 +9,8 @@
|
||||
#include "lang_auto.h"
|
||||
#include "settings_ayu_utils.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "filters/settings_filters_list.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/vertical_list.h"
|
||||
@@ -242,31 +244,6 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) {
|
||||
container->lifetime());
|
||||
}
|
||||
|
||||
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
AddSubsectionTitle(container, tr::ayu_RegexFilters());
|
||||
|
||||
AddButtonWithIcon(
|
||||
container,
|
||||
tr::ayu_FiltersHideFromBlocked(),
|
||||
st::settingsButtonNoIcon
|
||||
)->toggleOn(
|
||||
rpl::single(settings->hideFromBlocked)
|
||||
)->toggledValue(
|
||||
) | rpl::filter(
|
||||
[=](bool enabled)
|
||||
{
|
||||
return (enabled != settings->hideFromBlocked);
|
||||
}) | start_with_next(
|
||||
[=](bool enabled)
|
||||
{
|
||||
AyuSettings::set_hideFromBlocked(enabled);
|
||||
AyuSettings::save();
|
||||
},
|
||||
container->lifetime());
|
||||
}
|
||||
|
||||
void SetupOther(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
@@ -327,12 +304,6 @@ void AyuGhost::setupContent(not_null<Window::SessionController*> controller) {
|
||||
AddDivider(content);
|
||||
AddSkip(content);
|
||||
|
||||
SetupMessageFilters(content);
|
||||
|
||||
AddSkip(content);
|
||||
AddDivider(content);
|
||||
AddSkip(content);
|
||||
|
||||
SetupOther(content);
|
||||
AddSkip(content);
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@ class PainterHighQualityEnabler;
|
||||
|
||||
namespace Settings {
|
||||
|
||||
QString asBeta(const QString &text) {
|
||||
return text + QString(" β");
|
||||
}
|
||||
|
||||
rpl::producer<QString> asBeta(rpl::producer<QString> text) {
|
||||
return std::move(text) | rpl::map([=](const QString &val)
|
||||
{
|
||||
return val + " β";
|
||||
return asBeta(val);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ struct NestedEntry
|
||||
std::function<void(bool)> callback;
|
||||
};
|
||||
|
||||
QString asBeta(const QString &text);
|
||||
rpl::producer<QString> asBeta(rpl::producer<QString> text);
|
||||
|
||||
not_null<Ui::RpWidget*> AddInnerToggle(not_null<Ui::VerticalLayout*> container,
|
||||
|
||||
@@ -469,6 +469,21 @@ void SetupContextMenuElements(not_null<Ui::VerticalLayout*> container,
|
||||
AyuSettings::set_showMessageDetailsInContextMenu(index);
|
||||
AyuSettings::save();
|
||||
});
|
||||
if (settings->filtersEnabled) {
|
||||
AddChooseButtonWithIconAndRightText(
|
||||
container,
|
||||
controller,
|
||||
settings->showAddFilterInContextMenu,
|
||||
options,
|
||||
tr::ayu_RegexFilterQuickAdd(),
|
||||
tr::ayu_SettingsContextMenuTitle(),
|
||||
st::menuIconAddToFolder,
|
||||
[=](int index)
|
||||
{
|
||||
AyuSettings::set_showAddFilterInContextMenu(index);
|
||||
AyuSettings::save();
|
||||
});
|
||||
}
|
||||
|
||||
AddSkip(container);
|
||||
AddDividerText(container, tr::ayu_SettingsContextMenuDescription());
|
||||
@@ -652,4 +667,4 @@ void AyuChats::setupContent(not_null<Window::SessionController*> controller) {
|
||||
ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace Settings
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#include "settings_filters.h"
|
||||
|
||||
#include "lang_auto.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "ayu/ui/boxes/import_filters_box.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "boxes/abstract_box.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "core/application.h"
|
||||
#include "filters/per_dialog_filter.h"
|
||||
#include "filters/settings_filters_list.h"
|
||||
#include "inline_bots/bot_attach_web_view.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
rpl::producer<QString> AyuFilters::title() {
|
||||
return tr::ayu_CategoryFilters();
|
||||
}
|
||||
|
||||
void AyuFilters::fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) {
|
||||
addAction(
|
||||
tr::ayu_FiltersMenuSelectChat(tr::now),
|
||||
[=]
|
||||
{
|
||||
if (const auto window = Core::App().activeWindow()) {
|
||||
if (const auto controller = window->sessionController()) {
|
||||
auto types = InlineBots::PeerTypes();
|
||||
types |= InlineBots::PeerType::Bot;
|
||||
types |= InlineBots::PeerType::Group;
|
||||
types |= InlineBots::PeerType::Broadcast;
|
||||
|
||||
Window::ShowChooseRecipientBox(
|
||||
controller,
|
||||
[=](not_null<Data::Thread*> thread)
|
||||
{
|
||||
const auto peer = thread->peer();
|
||||
controller->dialogId = getDialogIdFromPeer(peer);
|
||||
controller->showExclude = true;
|
||||
controller->showSettings(AyuFiltersList::Id());
|
||||
return true;
|
||||
},
|
||||
tr::ayu_FiltersMenuSelectChat(),
|
||||
nullptr,
|
||||
types
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
&st::menuIconSearch);
|
||||
addAction({
|
||||
.isSeparator = true
|
||||
});
|
||||
addAction(
|
||||
tr::ayu_FiltersMenuImport(tr::now),
|
||||
[=]
|
||||
{
|
||||
auto box = Box(Ui::FillImportFiltersBox, true);
|
||||
Ui::show(std::move(box));
|
||||
},
|
||||
&st::menuIconArchive);
|
||||
if (AyuDatabase::hasFilters()) {
|
||||
addAction(
|
||||
tr::ayu_FiltersMenuExport(tr::now),
|
||||
[=]
|
||||
{
|
||||
auto box = Box(Ui::FillImportFiltersBox, false);
|
||||
Ui::show(std::move(box));
|
||||
},
|
||||
&st::menuIconUnarchive);
|
||||
}
|
||||
addAction({
|
||||
.isSeparator = true
|
||||
});
|
||||
addAction(
|
||||
tr::ayu_FiltersMenuClear(tr::now),
|
||||
[=]
|
||||
{
|
||||
auto callback = [=](Fn<void()> &&close)
|
||||
{
|
||||
AyuDatabase::deleteAllFilters();
|
||||
AyuDatabase::deleteAllExclusions();
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
close();
|
||||
};
|
||||
|
||||
auto box = Ui::MakeConfirmBox({
|
||||
.text = tr::ayu_FiltersClearPopupText(),
|
||||
.confirmed = callback,
|
||||
.confirmText = tr::ayu_FiltersClearPopupActionText()
|
||||
});
|
||||
Ui::show(std::move(box));
|
||||
},
|
||||
&st::menuIconClear);
|
||||
}
|
||||
|
||||
AyuFilters::AyuFilters(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: Section(parent) {
|
||||
setupContent(controller);
|
||||
}
|
||||
|
||||
void SetupFiltersSettings(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
AddSkip(container);
|
||||
AddSubsectionTitle(container, tr::ayu_RegexFilters());
|
||||
|
||||
AddButtonWithIcon(
|
||||
container,
|
||||
tr::ayu_RegexFiltersEnable(),
|
||||
st::settingsButtonNoIcon
|
||||
)->toggleOn(
|
||||
rpl::single(settings->filtersEnabled)
|
||||
)->toggledValue(
|
||||
) | rpl::filter(
|
||||
[=](bool enabled)
|
||||
{
|
||||
return (enabled != settings->filtersEnabled);
|
||||
}) | start_with_next(
|
||||
[=](bool enabled)
|
||||
{
|
||||
AyuSettings::set_filtersEnabled(enabled);
|
||||
AyuSettings::save();
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
container->lifetime());
|
||||
|
||||
AddButtonWithIcon(
|
||||
container,
|
||||
tr::ayu_RegexFiltersEnableSharedInChats(),
|
||||
st::settingsButtonNoIcon
|
||||
)->toggleOn(
|
||||
rpl::single(settings->filtersEnabledInChats)
|
||||
)->toggledValue(
|
||||
) | rpl::filter(
|
||||
[=](bool enabled)
|
||||
{
|
||||
return (enabled != settings->filtersEnabledInChats);
|
||||
}) | start_with_next(
|
||||
[=](bool enabled)
|
||||
{
|
||||
AyuSettings::set_filtersEnabledInChats(enabled);
|
||||
AyuSettings::save();
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
container->lifetime());
|
||||
|
||||
|
||||
AddButtonWithIcon(
|
||||
container,
|
||||
tr::ayu_FiltersHideFromBlocked(),
|
||||
st::settingsButtonNoIcon
|
||||
)->toggleOn(
|
||||
rpl::single(settings->hideFromBlocked)
|
||||
)->toggledValue(
|
||||
) | rpl::filter(
|
||||
[=](bool enabled)
|
||||
{
|
||||
return (enabled != settings->hideFromBlocked);
|
||||
}) | start_with_next(
|
||||
[=](bool enabled)
|
||||
{
|
||||
AyuSettings::set_hideFromBlocked(enabled);
|
||||
AyuSettings::save();
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
},
|
||||
container->lifetime());
|
||||
AddSkip(container);
|
||||
}
|
||||
|
||||
void SetupShared(not_null<Window::SessionController*> controller,
|
||||
Ui::VerticalLayout *container) {
|
||||
Ui::AddSkip(container);
|
||||
|
||||
auto button = container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
tr::ayu_RegexFiltersShared()
|
||||
));
|
||||
button->addClickHandler([=]
|
||||
{
|
||||
controller->dialogId = std::nullopt; // ensure we're handling shared filters
|
||||
controller->showExclude = false;
|
||||
controller->showSettings(AyuFiltersList::Id());
|
||||
});
|
||||
}
|
||||
|
||||
void SetupPerDialog(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<Ui::VerticalLayout*> container
|
||||
) {
|
||||
auto ctrl = container->lifetime().make_state<PerDialogFiltersListController>(
|
||||
&controller->session(),
|
||||
controller
|
||||
);
|
||||
|
||||
auto list = object_ptr<Ui::PaddingWrap<PeerListContent>>(
|
||||
container,
|
||||
object_ptr<PeerListContent>(
|
||||
container,
|
||||
ctrl),
|
||||
QMargins(0, -st::peerListBox.padding.top(), 0, -st::peerListBox.padding.bottom()));
|
||||
AddSkip(container);
|
||||
const auto content = container->add(std::move(list));
|
||||
AddSkip(container);
|
||||
auto delegate = container->lifetime().make_state<PeerListContentDelegateSimple>();
|
||||
delegate->setContent(content->entity());
|
||||
ctrl->setDelegate(delegate);
|
||||
}
|
||||
|
||||
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
AddSubsectionTitle(container, tr::ayu_RegexFilters());
|
||||
|
||||
AddButtonWithIcon(
|
||||
container,
|
||||
tr::ayu_FiltersHideFromBlocked(),
|
||||
st::settingsButtonNoIcon
|
||||
)->toggleOn(
|
||||
rpl::single(settings->hideFromBlocked)
|
||||
)->toggledValue(
|
||||
) | rpl::filter(
|
||||
[=](bool enabled)
|
||||
{
|
||||
return (enabled != settings->hideFromBlocked);
|
||||
}) | start_with_next(
|
||||
[=](bool enabled)
|
||||
{
|
||||
AyuSettings::set_hideFromBlocked(enabled);
|
||||
AyuSettings::save();
|
||||
},
|
||||
container->lifetime());
|
||||
}
|
||||
|
||||
void AyuFilters::setupContent(not_null<Window::SessionController*> controller) {
|
||||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
|
||||
SetupFiltersSettings(content);
|
||||
|
||||
AddDivider(content);
|
||||
|
||||
SetupShared(controller, content);
|
||||
|
||||
if (AyuDatabase::hasPerDialogFilters()) {
|
||||
AddSkip(content);
|
||||
AddDivider(content);
|
||||
SetupPerDialog(controller, content);
|
||||
}
|
||||
|
||||
ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
@@ -0,0 +1,30 @@
|
||||
// This is the source code of AyuGram for Desktop.
|
||||
//
|
||||
// We do not and cannot prevent the use of our code,
|
||||
// but be respectful and credit the original author.
|
||||
//
|
||||
// Copyright @Radolyn, 2025
|
||||
#pragma once
|
||||
|
||||
#include "settings/settings_common.h"
|
||||
#include "settings/settings_common_session.h"
|
||||
|
||||
namespace Window {
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Settings {
|
||||
|
||||
class AyuFilters : public Section<AyuFilters>
|
||||
{
|
||||
public:
|
||||
AyuFilters(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> title() override;
|
||||
void fillTopBarMenu(const Ui::Menu::MenuCallback &addAction) override;
|
||||
|
||||
private:
|
||||
void setupContent(not_null<Window::SessionController*> controller);
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
@@ -26,4 +26,4 @@ private:
|
||||
void setupContent(not_null<Window::SessionController*> controller);
|
||||
};
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace Settings
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
#include "lang_auto.h"
|
||||
#include "settings_appearance.h"
|
||||
#include "settings_ayu.h"
|
||||
#include "settings_ayu_utils.h"
|
||||
#include "settings_chats.h"
|
||||
#include "settings_filters.h"
|
||||
#include "settings_general.h"
|
||||
#include "settings_other.h"
|
||||
|
||||
@@ -87,7 +89,7 @@ void SetupCategories(
|
||||
|
||||
const auto categories = std::vector<CategoryInfo>{
|
||||
{QString("AyuGram"), &st::menuIconGroupReactions, [=] { showOther(AyuGhost::Id()); }},
|
||||
// {QString("Filters"), &st::menuIconTagFilter, [=] { showOther(AyuFilters::Id()); }},
|
||||
{asBeta(tr::ayu_CategoryFilters(tr::now)), &st::menuIconTagFilter, [=] { showOther(AyuFilters::Id()); }},
|
||||
{tr::ayu_CategoryGeneral(tr::now), &st::menuIconShowAll, [=] { showOther(AyuGeneral::Id()); }},
|
||||
{tr::ayu_CategoryAppearance(tr::now), &st::menuIconPalette, [=] { showOther(AyuAppearance::Id()); }},
|
||||
{tr::ayu_CategoryChats(tr::now), &st::menuIconChatBubble, [=] { showOther(AyuChats::Id()); }},
|
||||
|
||||
@@ -247,4 +247,4 @@ void AyuOther::setupContent(not_null<Window::SessionController*> controller) {
|
||||
ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
} // namespace Settings
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "telegram_helpers.h"
|
||||
|
||||
#include <functional>
|
||||
#include <latch>
|
||||
#include <QTimer>
|
||||
|
||||
#include "apiwrap.h"
|
||||
@@ -38,9 +39,12 @@
|
||||
#include "ayu/ayu_state.h"
|
||||
#include "ayu/data/messages_storage.h"
|
||||
#include "data/data_poll.h"
|
||||
#include "ayu/features/filters/filters_controller.h"
|
||||
#include "data/data_saved_sublist.h"
|
||||
#include "main/main_domain.h"
|
||||
|
||||
|
||||
#include "unicode/regex.h"
|
||||
namespace {
|
||||
|
||||
constexpr auto usernameResolverBotId = 8001593505L;
|
||||
@@ -132,24 +136,7 @@ bool isMessageHidden(const not_null<HistoryItem*> item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
if (settings.hideFromBlocked) {
|
||||
if (item->from()->isUser() &&
|
||||
item->from()->asUser()->isBlocked()) {
|
||||
// don't hide messages if it's a dialog with blocked user
|
||||
return item->from()->asUser()->id != item->history()->peer->id;
|
||||
}
|
||||
|
||||
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
|
||||
if (forwarded->originalSender &&
|
||||
forwarded->originalSender->isUser() &&
|
||||
forwarded->originalSender->asUser()->isBlocked()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return FiltersController::filtered(item);
|
||||
}
|
||||
|
||||
void MarkAsReadChatList(not_null<Dialogs::MainList*> list) {
|
||||
@@ -812,3 +799,73 @@ bool mediaDownloadable(const Data::Media *media) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void resolveAllChats(const std::map<long long, QString> &peers) {
|
||||
// not sure is this works
|
||||
auto session = currentSession();
|
||||
|
||||
crl::async([=, &session]
|
||||
{
|
||||
while (!peers.empty()) {
|
||||
for (const auto &[id, username] : peers) {
|
||||
std::latch latch(1);
|
||||
|
||||
auto onSuccess = [=, &latch](const MTPChatInvite &invite) {
|
||||
|
||||
invite.match([=](const MTPDchatInvite &data) {},
|
||||
[=](const MTPDchatInviteAlready &data) {
|
||||
if (const auto chat = session->data().processChat(data.vchat())) {
|
||||
if (const auto channel = chat->asChannel()) {
|
||||
channel->clearInvitePeek();
|
||||
}
|
||||
}
|
||||
}, [=](const MTPDchatInvitePeek &data) {});
|
||||
|
||||
latch.count_down();
|
||||
};
|
||||
auto onFail = [=, &latch](const MTP::Error &error) {
|
||||
if (MTP::IsFloodError(error.type())) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(20));
|
||||
}
|
||||
latch.count_down();
|
||||
};
|
||||
|
||||
session->api().checkChatInvite(username, onSuccess, onFail);
|
||||
latch.wait();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
not_null<Main::Session *> currentSession() {
|
||||
return &Core::App().domain().active().session();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
PeerData* getPeerFromDialogId(T id) {
|
||||
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
||||
if (const auto session = account->maybeSession()) {
|
||||
PeerData *from = session->data().userLoaded(id);
|
||||
if (!from) {
|
||||
from = session->data().channelLoaded(id);
|
||||
}
|
||||
if (!from) {
|
||||
from = reinterpret_cast<PeerData*>(session->data().chatLoaded(id));
|
||||
}
|
||||
|
||||
if (from) {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PeerData* getPeerFromDialogId(ID id) {
|
||||
return getPeerFromDialogId<ID>(id);
|
||||
}
|
||||
|
||||
PeerData* getPeerFromDialogId(unsigned long long id) {
|
||||
return getPeerFromDialogId<unsigned long long>(id);
|
||||
}
|
||||
|
||||
@@ -61,4 +61,10 @@ void searchById(ID userId, Main::Session *session, const UsernameResolverCallbac
|
||||
ID getUserIdFromPackId(uint64 id);
|
||||
|
||||
TextWithTags extractText(not_null<HistoryItem*> item);
|
||||
bool mediaDownloadable(const Data::Media* media);
|
||||
bool mediaDownloadable(const Data::Media* media);
|
||||
|
||||
void resolveAllChats(const std::map<long long, QString> &peers);
|
||||
not_null<Main::Session *> currentSession();
|
||||
|
||||
PeerData* getPeerFromDialogId(ID id);
|
||||
PeerData* getPeerFromDialogId(unsigned long long id);
|
||||
|
||||
@@ -85,6 +85,12 @@ public:
|
||||
|
||||
return _peer;
|
||||
}
|
||||
// AyuGram
|
||||
void setPeer(not_null<PeerData*> peer) {
|
||||
_peer = peer;
|
||||
}
|
||||
// AyuGram
|
||||
|
||||
[[nodiscard]] PeerListRowId id() const {
|
||||
return _id;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "ayu/ui/context_menu/context_menu.h"
|
||||
#include "ayu/ui/settings/filters/edit_filter.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_document_media.h"
|
||||
|
||||
@@ -2340,6 +2342,9 @@ void HistoryInner::contextMenuEvent(QContextMenuEvent *e) {
|
||||
}
|
||||
|
||||
void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
const auto &settings = AyuSettings::getInstance();
|
||||
|
||||
|
||||
if (e->reason() == QContextMenuEvent::Mouse) {
|
||||
mouseActionUpdate(e->globalPos());
|
||||
}
|
||||
@@ -2985,6 +2990,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
hasCopyRestrictionForSelected()));
|
||||
}, &st::menuIconTranslate);
|
||||
}
|
||||
AyuUi::AddCreateFilterAction(_menu, _controller, item, selectedText.rich.text);
|
||||
addItemActions(item, item);
|
||||
} else {
|
||||
addReplyAction(partItemOrLeader);
|
||||
|
||||
@@ -63,7 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/ayu_settings.h"
|
||||
|
||||
#include "ayu/features/filters/filters_controller.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -536,16 +536,21 @@ void HistoryMessageReply::updateData(
|
||||
&& author->isUser()
|
||||
&& author->asUser()->isBlocked();
|
||||
|
||||
|
||||
const auto filtered = resolvedMessage &&
|
||||
!resolvedMessage.empty() &&
|
||||
FiltersController::filtered(resolvedMessage.get());
|
||||
|
||||
const auto displaying = resolvedMessage
|
||||
|| resolvedStory
|
||||
|| ((nonEmptyQuote || _fields.externalMedia)
|
||||
&& (!_fields.messageId || force));
|
||||
_displaying = displaying && !blocked ? 1 : 0;
|
||||
_displaying = displaying && !blocked && !filtered ? 1 : 0;
|
||||
|
||||
const auto unavailable = !resolvedMessage
|
||||
&& !resolvedStory
|
||||
&& ((!_fields.storyId && !_fields.messageId) || force);
|
||||
_unavailable = unavailable && !blocked ? 1 : 0;
|
||||
_unavailable = unavailable && !blocked && !filtered ? 1 : 0;
|
||||
|
||||
if (force) {
|
||||
if (!_displaying && (_fields.messageId || _fields.storyId)) {
|
||||
|
||||
@@ -658,10 +658,10 @@ HistoryWidget::HistoryWidget(
|
||||
}, lifetime());
|
||||
|
||||
rpl::merge(
|
||||
AyuSettings::get_hideFromBlockedReactive() | rpl::to_empty,
|
||||
session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::IsBlocked
|
||||
) | rpl::to_empty
|
||||
) | rpl::to_empty,
|
||||
AyuSettings::get_filtersUpdate()
|
||||
) | rpl::start_with_next(
|
||||
[=]
|
||||
{
|
||||
@@ -676,6 +676,20 @@ HistoryWidget::HistoryWidget(
|
||||
}
|
||||
updateHistoryGeometry();
|
||||
update();
|
||||
|
||||
for (const auto &item : _history->blocks) {
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &msg : item->messages) {
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_history->owner().requestViewResize(msg.get());
|
||||
_history->owner().requestItemViewRefresh(msg->data());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1592,7 +1592,7 @@ void Element::destroyUnreadBar() {
|
||||
}
|
||||
|
||||
int Element::displayedDateHeight() const {
|
||||
if (AyuFeatures::MessageShot::isTakingShot()) {
|
||||
if (AyuFeatures::MessageShot::isTakingShot() || isMessageHidden(data())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2124,10 +2124,10 @@ Element *Element::previousInBlocks() const {
|
||||
|
||||
Element *Element::previousDisplayedInBlocks() const {
|
||||
auto result = previousInBlocks();
|
||||
while (result && (result->data()->isEmpty() || result->isHidden())) {
|
||||
while (result && ((result->data()->isEmpty() || result->isHidden()) && !isMessageHidden(data()))) {
|
||||
result = result->previousInBlocks();
|
||||
}
|
||||
return result;
|
||||
return result == this ? nullptr : result;
|
||||
}
|
||||
|
||||
Element *Element::nextInBlocks() const {
|
||||
@@ -2145,10 +2145,10 @@ Element *Element::nextInBlocks() const {
|
||||
|
||||
Element *Element::nextDisplayedInBlocks() const {
|
||||
auto result = nextInBlocks();
|
||||
while (result && (result->data()->isEmpty() || result->isHidden())) {
|
||||
while (result && ((result->data()->isEmpty() || result->isHidden()) && !isMessageHidden(data()))) {
|
||||
result = result->nextInBlocks();
|
||||
}
|
||||
return result;
|
||||
return result == this ? nullptr : result;
|
||||
}
|
||||
|
||||
void Element::drawInfo(
|
||||
|
||||
@@ -22,7 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/ayu_settings.h"
|
||||
|
||||
#include "ayu/features/filters/shadow_ban_utils.h"
|
||||
|
||||
namespace HistoryView {
|
||||
namespace {
|
||||
@@ -73,6 +73,9 @@ bool SendActionPainter::updateNeedsAnimating(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (ShadowBanUtils::isShadowBanned(user->id.value & PeerId::kChatTypeMask)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto now = crl::now();
|
||||
const auto emplaceAction = [&](
|
||||
|
||||
@@ -53,6 +53,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/ui/settings/filters/edit_filter.h"
|
||||
#include "ayu/ui/settings/filters/settings_filters_list.h"
|
||||
#include "styles/style_ayu_settings.h"
|
||||
|
||||
namespace Info {
|
||||
namespace {
|
||||
@@ -410,6 +415,31 @@ void WrapWidget::setupTopBarMenuToggle() {
|
||||
Box(Ui::FillPeerQrBox, self, std::nullopt, nullptr));
|
||||
});
|
||||
}
|
||||
} else if (section.settingsType() == ::Settings::AyuFiltersList::Id()) {
|
||||
const auto controller = _controller->parentController();
|
||||
const auto &st = st::filtersAddIcon;
|
||||
const auto button = _topBar->addButton(base::make_unique_q<Ui::IconButton>(_topBar, st));
|
||||
|
||||
const auto show = controller->uiShow();
|
||||
|
||||
button->addClickHandler(
|
||||
[=]
|
||||
{
|
||||
show->show(::Settings::RegexEditBox(nullptr, nullptr, controller->dialogId));
|
||||
});
|
||||
|
||||
|
||||
if (controller->showExclude.has_value() && controller->showExclude.value()) {
|
||||
auto icon = base::make_unique_q<Ui::IconButton>(_topBar, st::filtersExcludeIcon);
|
||||
|
||||
const auto excludeButton = _topBar->addButton(std::move(icon));
|
||||
excludeButton->addClickHandler([=, content = _content.data()]
|
||||
{
|
||||
// open new
|
||||
controller->showExclude = false;
|
||||
controller->showSettings(::Settings::AyuFiltersList::Id());
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (key.storiesPeer()
|
||||
&& key.storiesPeer()->isSelf()
|
||||
@@ -680,6 +710,21 @@ void WrapWidget::finishShowContent() {
|
||||
) | rpl::start_with_next([=] {
|
||||
updateContentGeometry();
|
||||
}, _content->lifetime());
|
||||
|
||||
AyuSettings::get_filtersUpdate() | rpl::start_with_next([=]
|
||||
{
|
||||
auto contentMemento = _content->createMemento();
|
||||
if (!contentMemento) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<ContentMemento>> stack;
|
||||
stack.push_back(std::move(contentMemento));
|
||||
const auto sectionMemento = std::make_shared<Memento>(std::move(stack));
|
||||
|
||||
showBackFromStackInternal(Window::SectionShow(anim::type::instant));
|
||||
showInternal(sectionMemento.get(), Window::SectionShow(anim::type::instant));
|
||||
}, _content->lifetime());
|
||||
}
|
||||
|
||||
rpl::producer<bool> WrapWidget::topShadowToggledValue() const {
|
||||
|
||||
@@ -11,12 +11,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "settings/settings_chat.h"
|
||||
#include "settings/settings_main.h"
|
||||
|
||||
// AyuGram includes
|
||||
#include "ayu/ui/settings/settings_filters.h"
|
||||
|
||||
|
||||
namespace Settings {
|
||||
|
||||
bool HasMenu(Type type) {
|
||||
return (type == ::Settings::CloudPasswordEmailConfirmId())
|
||||
|| (type == Main::Id())
|
||||
|| (type == Chat::Id());
|
||||
|| (type == Chat::Id())
|
||||
|| (type == AyuFilters::Id());
|
||||
}
|
||||
|
||||
} // namespace Settings
|
||||
|
||||
@@ -1579,6 +1579,7 @@ void Filler::fillProfileActions() {
|
||||
addManageTopic();
|
||||
addToggleTopicClosed();
|
||||
AyuUi::AddOpenChannelAction(_peer, _controller, _addAction);
|
||||
AyuUi::AddShadowBanAction(_peer, _addAction);
|
||||
addViewDiscussion();
|
||||
addDirectMessages();
|
||||
addExportChat();
|
||||
|
||||
@@ -678,6 +678,10 @@ public:
|
||||
return _lifetime;
|
||||
}
|
||||
|
||||
// AyuGram filters
|
||||
std::optional<long long> dialogId;
|
||||
std::vector<char> filterId;
|
||||
std::optional<bool> showExclude; // whether to show exclude button in the top bar
|
||||
private:
|
||||
struct CachedThemeKey;
|
||||
struct CachedTheme;
|
||||
|
||||
Submodule
+1
Submodule Telegram/lib_icu added at 6c828925ef
Reference in New Issue
Block a user