mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-09-24 12:45:22 +02:00
feat(WIP): message filters
This commit is contained in:
parent
4acc69e6ec
commit
8a1fdf2943
40 changed files with 2746 additions and 28 deletions
|
@ -148,6 +148,12 @@ set(ayugram_files
|
|||
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/peer_global_exclusion.cpp
|
||||
ayu/ui/settings/filters/peer_global_exclusion.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
|
||||
|
@ -205,6 +211,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
|
||||
|
|
BIN
Telegram/Resources/icons/ayu/add.png
Normal file
BIN
Telegram/Resources/icons/ayu/add.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 236 B |
BIN
Telegram/Resources/icons/ayu/add@2x.png
Normal file
BIN
Telegram/Resources/icons/ayu/add@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 310 B |
BIN
Telegram/Resources/icons/ayu/add@3x.png
Normal file
BIN
Telegram/Resources/icons/ayu/add@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 400 B |
|
@ -7000,6 +7000,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
"ayu_FiltersHideFromBlockedNote" = "Message filters were enabled.";
|
||||
"ayu_RegexFiltersEnableSharedInChats" = "Enable Shared Filters in Chats";
|
||||
"ayu_RegexFiltersAdd" = "Add Filter";
|
||||
"ayu_RegexFiltersAddDescription" = "You can use {link} to test and debug your regular expression.";
|
||||
"ayu_RegexFiltersAddDescription_link" = "regex101.com";
|
||||
"ayu_RegexFiltersEdit" = "Edit Filter";
|
||||
"ayu_RegexFiltersPlaceholder" = "Expression";
|
||||
"ayu_EnableExpression" = "Enable Filter";
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
@ -52,12 +53,16 @@ void initTranslator() {
|
|||
Ayu::Translator::TranslateManager::init();
|
||||
}
|
||||
|
||||
void initShadowBan() {
|
||||
ShadowBanUtils::reloadShadowBan();
|
||||
}
|
||||
void init() {
|
||||
initLang();
|
||||
initDatabase();
|
||||
initUiSettings();
|
||||
initWorker();
|
||||
initRCManager();
|
||||
initShadowBan();
|
||||
initTranslator();
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,13 @@ rpl::variable<int> showPeerIdReactive;
|
|||
|
||||
rpl::variable<QString> translationProviderReactive;
|
||||
|
||||
rpl::variable<bool> filtersEnabledReactive;
|
||||
rpl::variable<bool> filtersEnabledInChatsReactive;
|
||||
rpl::variable<QString> shadowBanIdsReactive;
|
||||
rpl::variable<bool> hideFromBlockedReactive;
|
||||
|
||||
rpl::event_stream<> filtersUpdateReactive; // triggered on adding / editing filter
|
||||
|
||||
rpl::event_stream<> historyUpdateReactive;
|
||||
|
||||
rpl::lifetime lifetime = rpl::lifetime();
|
||||
|
@ -142,6 +148,9 @@ void postinitialize() {
|
|||
showPeerIdReactive = settings->showPeerId;
|
||||
translationProviderReactive = settings->translationProvider;
|
||||
|
||||
filtersEnabledReactive = settings->filtersEnabled;
|
||||
filtersEnabledInChatsReactive = settings->filtersEnabledInChats;
|
||||
shadowBanIdsReactive = settings->shadowBanIds;
|
||||
hideFromBlockedReactive = settings->hideFromBlocked;
|
||||
|
||||
ghostModeEnabled = ghostModeEnabled_util(settings.value());
|
||||
|
@ -226,6 +235,8 @@ AyuGramSettings::AyuGramSettings() {
|
|||
saveForBots = false;
|
||||
|
||||
// ~ Message filters
|
||||
filtersEnabled = false;
|
||||
filtersEnabledInChats = false;
|
||||
hideFromBlocked = false;
|
||||
|
||||
// ~ QoL toggles
|
||||
|
@ -397,6 +408,20 @@ void set_saveForBots(bool val) {
|
|||
settings->saveForBots = val;
|
||||
}
|
||||
|
||||
void set_filtersEnabled(bool val) {
|
||||
settings->filtersEnabled = val;
|
||||
filtersEnabledReactive = val;
|
||||
}
|
||||
void set_filtersEnabledInChats(bool val) {
|
||||
settings->filtersEnabledInChats = val;
|
||||
filtersEnabledInChatsReactive = val;
|
||||
}
|
||||
|
||||
void set_shadowBanIds(const QString &val) {
|
||||
settings->shadowBanIds = val;
|
||||
shadowBanIdsReactive = val;
|
||||
}
|
||||
|
||||
void set_hideFromBlocked(bool val) {
|
||||
settings->hideFromBlocked = val;
|
||||
hideFromBlockedReactive = val;
|
||||
|
@ -694,9 +719,25 @@ rpl::producer<bool> get_ghostModeEnabledReactive() {
|
|||
return ghostModeEnabled.value();
|
||||
}
|
||||
|
||||
rpl::producer<bool> get_filtersEnabledReactive() {
|
||||
return filtersEnabledReactive.value();
|
||||
}
|
||||
rpl::producer<bool> get_filtersEnabledInChatsReactive() {
|
||||
return filtersEnabledInChatsReactive.value();
|
||||
}
|
||||
rpl::producer<QString> get_shadowBanIdsReactive() {
|
||||
return shadowBanIdsReactive.value();
|
||||
}
|
||||
|
||||
rpl::producer<bool> get_hideFromBlockedReactive() {
|
||||
return hideFromBlockedReactive.value();
|
||||
}
|
||||
void fire_filtersUpdate() {
|
||||
filtersUpdateReactive.fire({});
|
||||
}
|
||||
rpl::producer<> get_filtersUpdate() {
|
||||
return filtersUpdateReactive.events();
|
||||
}
|
||||
|
||||
void triggerHistoryUpdate() {
|
||||
historyUpdateReactive.fire({});
|
||||
|
|
|
@ -59,6 +59,9 @@ public:
|
|||
|
||||
bool saveForBots;
|
||||
|
||||
QString shadowBanIds;
|
||||
bool filtersEnabled;
|
||||
bool filtersEnabledInChats;
|
||||
bool hideFromBlocked;
|
||||
|
||||
bool disableAds;
|
||||
|
@ -160,6 +163,9 @@ void set_saveMessagesHistory(bool val);
|
|||
|
||||
void set_saveForBots(bool val);
|
||||
|
||||
void set_filtersEnabled(bool val);
|
||||
void set_filtersEnabledInChats(bool val);
|
||||
void set_shadowBanIds(const QString &val);
|
||||
void set_hideFromBlocked(bool val);
|
||||
|
||||
void set_disableAds(bool val);
|
||||
|
@ -255,6 +261,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)
|
||||
|
@ -334,6 +343,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)
|
||||
|
@ -418,8 +430,14 @@ bool isUseScheduledMessages();
|
|||
|
||||
rpl::producer<bool> get_ghostModeEnabledReactive();
|
||||
|
||||
rpl::producer<bool> get_filtersEnabledReactive();
|
||||
rpl::producer<bool> get_filtersEnabledInChatsReactive();
|
||||
rpl::producer<QString> get_shadowBanIdsReactive();
|
||||
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,165 @@ 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()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,4 +20,28 @@ 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();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
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,29 @@
|
|||
// 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 "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();
|
||||
|
||||
}
|
||||
|
188
Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp
Normal file
188
Telegram/SourceFiles/ayu/features/filters/filters_controller.cpp
Normal file
|
@ -0,0 +1,188 @@
|
|||
// 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 <functional>
|
||||
#include <QTimer>
|
||||
|
||||
#include "apiwrap.h"
|
||||
|
||||
#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"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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()) {
|
||||
return matches(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
return settings.filtersEnabled && (settings.filtersEnabledInChats || peer->asChannel());
|
||||
}
|
||||
|
||||
bool isBlocked(const not_null<HistoryItem *> item) {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
|
||||
ID peer = 0;
|
||||
if (const auto user = item->from()->asUser()) {
|
||||
peer = user->id.value & PeerId::kChatTypeMask;
|
||||
}
|
||||
|
||||
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 &&
|
||||
(
|
||||
ShadowBanUtils::isShadowBanned(peer) ||
|
||||
settings.hideFromBlocked && blocked
|
||||
);
|
||||
}
|
||||
|
||||
// unused, probably need to remove
|
||||
bool filteredWithoutCaching(const not_null<HistoryItem *> item) {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
if (!settings.filtersEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item->out()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filterBlocked(item)) return true;
|
||||
|
||||
if (!isEnabled(item->from())) return false;
|
||||
|
||||
const auto cached = FiltersCacheController::isFiltered(item);
|
||||
if (cached.has_value()) {
|
||||
return cached.value();
|
||||
}
|
||||
const auto filtered = isFiltered(FilterUtils::extractAllText(item), item->id.bare);
|
||||
if (filtered.has_value()) {
|
||||
return filtered.value();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Main Method
|
||||
bool filtered(const not_null<HistoryItem *> item) {
|
||||
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), item->history()->peer->id.value & PeerId::kChatTypeMask);
|
||||
|
||||
// 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,52 @@
|
|||
// 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 <vector>
|
||||
#include <string>
|
||||
#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;
|
||||
// && pattern.pattern == other.pattern.pattern
|
||||
// && pattern.reversed == other.pattern.reversed;
|
||||
}
|
||||
};
|
||||
|
||||
// im unable to override std::hash of HashablePattern (skill issue) so use hasher
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
509
Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp
Normal file
509
Telegram/SourceFiles/ayu/features/filters/filters_utils.cpp
Normal file
|
@ -0,0 +1,509 @@
|
|||
// 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 <lang_auto.h>
|
||||
#include <QJsonArray>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <vector>
|
||||
|
||||
#include "filters_cache_controller.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_document.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"
|
||||
|
||||
|
||||
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 QByteArray response = _reply->readAll();
|
||||
|
||||
if (!handleResponse(response)) {
|
||||
LOG(("Filters import: Error handling response or bad map size: %1").arg(response.size()));
|
||||
}
|
||||
|
||||
_reply->deleteLater();
|
||||
});
|
||||
|
||||
connect(_reply, &QNetworkReply::errorOccurred, this, [=](QNetworkReply::NetworkError e) {
|
||||
gotFailure(e);
|
||||
|
||||
_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) {
|
||||
LOG(("FilterUtils: Failed to parse JSON, error: %1"
|
||||
).arg(error.errorString()));
|
||||
return false;
|
||||
}
|
||||
if (!document.isObject()) {
|
||||
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;
|
||||
json["filterId"] = QString::fromUtf8(filterId);
|
||||
return json;
|
||||
}
|
||||
};
|
||||
QString FilterUtils::exportFilters() {
|
||||
auto createJsonArray = [&](const auto &container) {
|
||||
QJsonArray jsonArray;
|
||||
for (const auto &item : container) {
|
||||
jsonArray.append(item.toJson());
|
||||
}
|
||||
return jsonArray;
|
||||
};
|
||||
|
||||
|
||||
QJsonObject jsonObject;
|
||||
jsonObject["version"] = BACKUP_VERSION;
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
jsonObject["filters"] = createJsonArray(filters);
|
||||
|
||||
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);
|
||||
|
||||
QJsonObject peers;
|
||||
for (const auto &item : filters) {
|
||||
const auto ¤tSession = Core::App().domain().active().session();
|
||||
if (!item.dialogId.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (const auto peer = currentSession.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").toInteger();
|
||||
} else {
|
||||
regex.dialogId = std::nullopt;
|
||||
}
|
||||
regex.enabled = filter.value("enabled").toBool();
|
||||
|
||||
auto byteArray = filter.value("id").toString().toUtf8();
|
||||
regex.id = std::vector(byteArray.constData(), byteArray.constData() + byteArray.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").toInteger();
|
||||
|
||||
auto byteArray = exclusion.value("filterId").toString().toUtf8();
|
||||
regex.filterId = std::vector(byteArray.constData(), byteArray.constData() + byteArray.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();
|
||||
|
||||
}
|
||||
|
83
Telegram/SourceFiles/ayu/features/filters/filters_utils.h
Normal file
83
Telegram/SourceFiles/ayu/features/filters/filters_utils.h
Normal file
|
@ -0,0 +1,83 @@
|
|||
// 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 "api/api_bot.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_user.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item_components.h"
|
||||
#include "main/main_session.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_widgets.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);
|
||||
|
||||
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,64 @@
|
|||
// 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"
|
||||
|
||||
std::unordered_set<ID> ShadowBanUtils::shadowBanList;
|
||||
|
||||
void ShadowBanUtils::reloadShadowBan() {
|
||||
loadShadowBanList();
|
||||
}
|
||||
|
||||
|
||||
void ShadowBanUtils::addShadowBan(ID userId) {
|
||||
if (shadowBanList.insert(userId).second) {
|
||||
setShadowBanList();
|
||||
}
|
||||
}
|
||||
|
||||
void ShadowBanUtils::removeShadowBan(ID userId) {
|
||||
if (shadowBanList.erase(userId) > 0) {
|
||||
setShadowBanList();
|
||||
}
|
||||
}
|
||||
|
||||
bool ShadowBanUtils::isShadowBanned(ID userId) {
|
||||
return shadowBanList.contains(userId);
|
||||
}
|
||||
|
||||
void ShadowBanUtils::loadShadowBanList() {
|
||||
auto &settings = AyuSettings::getInstance();
|
||||
|
||||
if (settings.shadowBanIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto idList = settings.shadowBanIds.split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (const auto &id : idList) {
|
||||
shadowBanList.insert(id.toLongLong());
|
||||
}
|
||||
}
|
||||
const std::unordered_set<ID> &ShadowBanUtils::getShadowBanList() {
|
||||
return shadowBanList;
|
||||
}
|
||||
void ShadowBanUtils::setShadowBanList() {
|
||||
QStringList idStringList;
|
||||
idStringList.reserve(shadowBanList.size());
|
||||
for (const auto &id : shadowBanList) {
|
||||
idStringList.push_back(QString::number(id));
|
||||
}
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
|
||||
AyuSettings::set_shadowBanIds(idStringList.join(","));
|
||||
AyuSettings::save();
|
||||
}
|
24
Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h
Normal file
24
Telegram/SourceFiles/ayu/features/filters/shadow_ban_utils.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
// 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 reloadShadowBan();
|
||||
static void addShadowBan(long long userId);
|
||||
static void removeShadowBan(long long userId);
|
||||
static bool isShadowBanned(long long userId);
|
||||
|
||||
private:
|
||||
ShadowBanUtils() = delete;
|
||||
|
||||
static const std::unordered_set<long long> &getShadowBanList();
|
||||
static void loadShadowBanList();
|
||||
static void setShadowBanList();
|
||||
static std::unordered_set<long long> shadowBanList;
|
||||
};
|
|
@ -10,6 +10,7 @@
|
|||
using "ui/colors.palette";
|
||||
using "ui/widgets/widgets.style";
|
||||
using "dialogs/dialogs.style";
|
||||
using "info/info.style";
|
||||
|
||||
ayuGhostIcon: icon {{ "ayu/ghost", menuIconColor }};
|
||||
ayuMenuIcon: icon {{ "ayu/ayu_menu", menuIconColor }};
|
||||
|
@ -52,3 +53,15 @@ dialogsExteraSupporterIcon: ThreeStateIcon {
|
|||
over: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgOver }};
|
||||
active: icon {{ "ayu/dialogs_extera_supporter", dialogsVerifiedIconBgActive }};
|
||||
}
|
||||
ayuFiltersAddIcon: IconButton(infoLayerTopBarClose) {
|
||||
width: 40px;
|
||||
icon: icon {{ "menu/add", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "menu/add", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
||||
ayuFiltersExcludeIcon: IconButton(infoLayerTopBarClose) {
|
||||
width: 40px;
|
||||
icon: icon {{ "ayu/streamer", boxTitleCloseFg }};
|
||||
iconOver: icon {{ "ayu/streamer", boxTitleCloseFgOver }};
|
||||
iconPosition: point(8px, -1px);
|
||||
}
|
|
@ -13,6 +13,7 @@
|
|||
#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"
|
||||
|
@ -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,32 @@ 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 = peerData->id.value & PeerId::kChatTypeMask;
|
||||
const auto toggleShadowBan = [=]
|
||||
{
|
||||
if (ShadowBanUtils::isShadowBanned(realId)) {
|
||||
ShadowBanUtils::removeShadowBan(realId);
|
||||
} else {
|
||||
ShadowBanUtils::addShadowBan(realId);
|
||||
}
|
||||
};
|
||||
|
||||
addCallback({
|
||||
.text = (ShadowBanUtils::isShadowBanned(realId)
|
||||
? tr::ayu_FiltersQuickUnshadowBan(tr::now)
|
||||
: tr::ayu_FiltersQuickShadowBan(tr::now)),
|
||||
.handler = toggleShadowBan,
|
||||
.icon = &st::menuIconStealth,
|
||||
});
|
||||
}
|
||||
|
||||
void AddDeleteOwnMessagesAction(PeerData *peerData,
|
||||
Data::ForumTopic *topic,
|
||||
not_null<Window::SessionController*> sessionController,
|
||||
|
|
|
@ -25,6 +25,8 @@ 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);
|
||||
|
|
434
Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp
Normal file
434
Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.cpp
Normal file
|
@ -0,0 +1,434 @@
|
|||
// 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 <styles/style_layers.h>
|
||||
#include <ui/widgets/fields/masked_input_field.h>
|
||||
#include <styles/style_window.h>
|
||||
#include <ui/toast/toast.h>
|
||||
#include <ui/text/text_utilities.h>
|
||||
|
||||
#include "ayu/ayu_settings.h"
|
||||
|
||||
#include "lang_auto.h"
|
||||
|
||||
#include "boxes/connection_box.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "styles/style_settings.h"
|
||||
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "ui/qt_object_factory.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "ui/vertical_list.h"
|
||||
#include "ui/widgets/checkbox.h"
|
||||
#include "ui/widgets/fields/input_field.h"
|
||||
|
||||
#include "ui/wrap/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "ui/widgets/fields/password_input.h"
|
||||
#include "ui/widgets/labels.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "styles/style_window.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/text.h"
|
||||
#include "media/view/media_view_overlay_widget.h"
|
||||
|
||||
#include "apiwrap.h"
|
||||
#include "api/api_attached_stickers.h"
|
||||
#include "api/api_peer_photo.h"
|
||||
#include "base/qt/qt_common_adapters.h"
|
||||
#include "base/timer_rpl.h"
|
||||
#include "lang/lang_keys.h"
|
||||
#include "menu/menu_sponsored.h"
|
||||
#include "boxes/premium_preview_box.h"
|
||||
#include "core/application.h"
|
||||
#include "core/click_handler_types.h"
|
||||
#include "core/file_utilities.h"
|
||||
#include "core/mime_type.h"
|
||||
#include "core/ui_integration.h"
|
||||
#include "core/crash_reports.h"
|
||||
#include "core/sandbox.h"
|
||||
#include "core/shortcuts.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||
#include "ui/widgets/dropdown_menu.h"
|
||||
#include "ui/widgets/popup_menu.h"
|
||||
#include "ui/widgets/buttons.h"
|
||||
#include "ui/layers/layer_manager.h"
|
||||
#include "ui/text/text_utilities.h"
|
||||
#include "ui/platform/ui_platform_window_title.h"
|
||||
#include "ui/toast/toast.h"
|
||||
#include "ui/text/format_values.h"
|
||||
#include "ui/item_text_options.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "ui/power_saving.h"
|
||||
#include "ui/cached_round_corners.h"
|
||||
#include "ui/gl/gl_window.h"
|
||||
#include "ui/boxes/confirm_box.h"
|
||||
#include "ui/ui_utility.h"
|
||||
#include "info/info_memento.h"
|
||||
#include "info/info_controller.h"
|
||||
#include "info/statistics/info_statistics_widget.h"
|
||||
#include "boxes/delete_messages_box.h"
|
||||
#include "boxes/report_messages_box.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/view/media_view_group_thumbs.h"
|
||||
#include "media/view/media_view_pip.h"
|
||||
#include "media/view/media_view_overlay_raster.h"
|
||||
#include "media/view/media_view_overlay_opengl.h"
|
||||
#include "media/view/media_view_playback_sponsored.h"
|
||||
#include "media/stories/media_stories_share.h"
|
||||
#include "media/stories/media_stories_view.h"
|
||||
#include "media/streaming/media_streaming_document.h"
|
||||
#include "media/streaming/media_streaming_player.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "history/history.h"
|
||||
#include "history/history_item_helpers.h"
|
||||
#include "history/view/media/history_view_media.h"
|
||||
#include "history/view/reactions/history_view_reactions_selector.h"
|
||||
#include "data/components/sponsored_messages.h"
|
||||
#include "data/data_session.h"
|
||||
#include "data/data_changes.h"
|
||||
#include "data/data_channel.h"
|
||||
#include "data/data_chat.h"
|
||||
#include "data/data_user.h"
|
||||
#include "data/data_media_rotation.h"
|
||||
#include "data/data_photo_media.h"
|
||||
#include "data/data_document_media.h"
|
||||
#include "data/data_document_resolver.h"
|
||||
#include "data/data_file_click_handler.h"
|
||||
#include "data/data_download_manager.h"
|
||||
#include "window/themes/window_theme_preview.h"
|
||||
#include "window/window_peer_menu.h"
|
||||
#include "window/window_controller.h"
|
||||
#include "base/platform/base_platform_info.h"
|
||||
#include "base/power_save_blocker.h"
|
||||
#include "base/random.h"
|
||||
#include "base/unixtime.h"
|
||||
#include "base/qt_signal_producer.h"
|
||||
#include "base/event_filter.h"
|
||||
#include "main/main_account.h"
|
||||
#include "main/main_domain.h" // Domain::activeSessionValue.
|
||||
#include "main/main_session.h"
|
||||
#include "main/main_session_settings.h"
|
||||
#include "layout/layout_document_generic_preview.h"
|
||||
#include "platform/platform_overlay_widget.h"
|
||||
#include "storage/file_download.h"
|
||||
#include "storage/storage_account.h"
|
||||
#include "calls/calls_instance.h"
|
||||
#include "styles/style_media_view.h"
|
||||
#include "styles/style_calls.h"
|
||||
#include "styles/style_chat.h"
|
||||
#include "styles/style_menu_icons.h"
|
||||
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtCore/QBuffer>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QWindow>
|
||||
#include <QtGui/QScreen>
|
||||
|
||||
#include <kurlmimedata.h>
|
||||
|
||||
class PainterHighQualityEnabler;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
rpl::producer<QString> AyuEditFilters::title()
|
||||
{
|
||||
return tr::ayu_RegexFiltersAdd();
|
||||
}
|
||||
|
||||
object_ptr<Ui::Checkbox> getCheckBox(not_null<Ui::VerticalLayout *> container,
|
||||
const QString &label,
|
||||
bool checked)
|
||||
{
|
||||
return object_ptr<Ui::Checkbox>(
|
||||
container,
|
||||
label,
|
||||
checked,
|
||||
st::settingsCheckbox);
|
||||
}
|
||||
|
||||
AyuEditFilters::AyuEditFilters(
|
||||
QWidget *parent,
|
||||
not_null<Window::SessionController *> controller)
|
||||
: Section(parent)
|
||||
{
|
||||
if (!controller->filterId.empty()) {
|
||||
currentFilter = AyuDatabase::getById(controller->filterId);
|
||||
}
|
||||
setupContent(controller, parent);
|
||||
}
|
||||
|
||||
// unused currently, TODO: need to add this on bad regex pattern
|
||||
not_null<Ui::FlatLabel *> AddError(
|
||||
not_null<Ui::VerticalLayout *> content,
|
||||
Ui::PasswordInput *input)
|
||||
{
|
||||
const auto error = content->add(
|
||||
object_ptr<Ui::CenterWrap<Ui::FlatLabel>>(
|
||||
content,
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
content,
|
||||
// Set any text to resize.
|
||||
tr::lng_language_name(tr::now),
|
||||
st::settingLocalPasscodeError)),
|
||||
st::changePhoneDescriptionPadding)->entity();
|
||||
error->hide();
|
||||
if (input) {
|
||||
QObject::connect(input,
|
||||
&Ui::MaskedInputField::changed,
|
||||
[=]
|
||||
{
|
||||
error->hide();
|
||||
});
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
void AyuEditFilters::setupSettings(not_null<Ui::VerticalLayout *> container, QWidget *parent)
|
||||
{
|
||||
AddSkip(container);
|
||||
|
||||
const auto add = [&](const QString &label, bool checked, auto &&handle)
|
||||
{
|
||||
auto check = container->add(
|
||||
getCheckBox(container, label, checked),
|
||||
st::settingsCheckboxPadding
|
||||
);
|
||||
check->checkedChanges(
|
||||
) | rpl::start_with_next(
|
||||
std::forward<decltype(handle)>(handle),
|
||||
container->lifetime());
|
||||
return check;
|
||||
};
|
||||
|
||||
|
||||
const auto name = container->add(
|
||||
object_ptr<Ui::InputField>(
|
||||
parent,
|
||||
st::windowFilterNameInput,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::ayu_RegexFiltersPlaceholder()),
|
||||
st::markdownLinkFieldPadding);
|
||||
|
||||
|
||||
auto enabled = add(
|
||||
QString("Enable Filter"),
|
||||
currentFilter.enabled,
|
||||
[=, this](bool checked)
|
||||
{
|
||||
currentFilter.enabled = checked;
|
||||
});
|
||||
|
||||
auto insensetive = add(
|
||||
QString("Case Insensitive"),
|
||||
currentFilter.caseInsensitive,
|
||||
[=, this](bool checked)
|
||||
{
|
||||
currentFilter.caseInsensitive = checked;
|
||||
});
|
||||
|
||||
auto reversed = add(
|
||||
QString("Reversed"),
|
||||
currentFilter.reversed,
|
||||
[=, this](bool checked)
|
||||
{
|
||||
currentFilter.reversed = checked;
|
||||
});
|
||||
|
||||
name->setText(QString::fromStdString(currentFilter.text));
|
||||
name->submits(
|
||||
) | rpl::start_with_next([=, this]
|
||||
{
|
||||
currentFilter.text = name->getTextWithTags().text.toStdString();
|
||||
|
||||
currentFilter.enabled = enabled->checked();
|
||||
currentFilter.caseInsensitive = insensetive->checked();
|
||||
currentFilter.reversed = reversed->checked();
|
||||
|
||||
if (currentFilter.id.empty()) {
|
||||
currentFilter.id = generate_uuid_bytes();
|
||||
}
|
||||
AyuDatabase::addRegexFilter(currentFilter);
|
||||
|
||||
FiltersCacheController::rebuildCache();
|
||||
},
|
||||
name->lifetime());
|
||||
}
|
||||
|
||||
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 name = box->addRow(
|
||||
object_ptr<Ui::InputField>(
|
||||
box->verticalLayout(),
|
||||
st::windowFilterNameInput,
|
||||
Ui::InputField::Mode::MultiLine,
|
||||
tr::ayu_RegexFiltersPlaceholder()),
|
||||
st::markdownLinkFieldPadding);
|
||||
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);
|
||||
|
||||
name->setText(QString::fromStdString(data.text));
|
||||
|
||||
auto saveAndClose = [=, id = data.id]
|
||||
{
|
||||
RegexFilter newFilter;
|
||||
newFilter.text = name->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;
|
||||
};
|
||||
Ui::Toast::Show(Ui::Toast::Config{
|
||||
// .text = tr::ayu_RegexFilterBulletinText(
|
||||
// tr::now,
|
||||
// lt_link,
|
||||
// Ui::Text::Link(
|
||||
// Ui::Text::Bold(
|
||||
// tr::ayu_RegexFilterBulletinAction(tr::now))),
|
||||
// Ui::Text::RichLangValue),
|
||||
|
||||
// TODO: reconsider
|
||||
.text = tr::ayu_RegexFilterBulletinText(
|
||||
tr::now
|
||||
//,
|
||||
// lt_link,
|
||||
// Ui::Text::Link(
|
||||
// Ui::Text::Bold(
|
||||
// tr::ayu_RegexFilterBulletinAction(tr::now))),
|
||||
// Ui::Text::WithEntities
|
||||
),
|
||||
.filter = onClick,
|
||||
.adaptive = true
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
name->submits() | rpl::start_with_next(saveAndClose, name->lifetime());
|
||||
box->addButton(tr::lng_settings_save(), saveAndClose);
|
||||
box->addButton(tr::lng_cancel(), [=]
|
||||
{ box->closeBox(); });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void AyuEditFilters::setupContent(not_null<Window::SessionController *> controller, QWidget *parent)
|
||||
{
|
||||
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
|
||||
|
||||
setupSettings(content, parent);
|
||||
|
||||
ResizeFitChild(this, content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
43
Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h
Normal file
43
Telegram/SourceFiles/ayu/ui/settings/filters/edit_filter.h
Normal file
|
@ -0,0 +1,43 @@
|
|||
// 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 "ui/wrap/vertical_layout.h"
|
||||
#include "chat_helpers/emoji_suggestions_widget.h"
|
||||
#include "boxes/premium_limits_box.h"
|
||||
#include "info/profile/info_profile_values.h"
|
||||
#include "window/window_session_controller.h"
|
||||
#include "base/unixtime.h"
|
||||
|
||||
class BoxContent;
|
||||
|
||||
namespace Window {
|
||||
class Controller;
|
||||
class SessionController;
|
||||
} // namespace Window
|
||||
|
||||
namespace Settings {
|
||||
|
||||
class AyuEditFilters : public Section<AyuEditFilters>
|
||||
{
|
||||
public:
|
||||
AyuEditFilters(QWidget *parent, not_null<Window::SessionController*> controller);
|
||||
void setupSettings(not_null<Ui::VerticalLayout*> container, QWidget *parent);
|
||||
|
||||
[[nodiscard]] rpl::producer<QString> title() override;
|
||||
|
||||
private:
|
||||
void setupContent(not_null<Window::SessionController*> controller, QWidget *parent);
|
||||
|
||||
RegexFilter currentFilter;
|
||||
};
|
||||
|
||||
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,169 @@
|
|||
// 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 "peer_global_exclusion.h"
|
||||
|
||||
#include "data/data_peer.h"
|
||||
#include "main/main_session.h"
|
||||
#include "styles/style_boxes.h"
|
||||
#include "ui/painter.h"
|
||||
|
||||
#include <lang_auto.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "settings_filters_list.h"
|
||||
#include "ayu/data/ayu_database.h"
|
||||
#include "ayu/utils/telegram_helpers.h"
|
||||
#include "data/data_session.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Settings {
|
||||
|
||||
GlobalExclusionListRow::GlobalExclusionListRow(PeerId peer)
|
||||
: PeerListRow(peer.value)
|
||||
, peerId(peer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QString GlobalExclusionListRow::generateName() {
|
||||
if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) {
|
||||
this->setPeer(peerCached);
|
||||
return PeerListRow::generateName();
|
||||
}
|
||||
return QString("UNKNOWN (ID: %1)").arg(QString::number(peerId.value & PeerId::kChatTypeMask));
|
||||
|
||||
}
|
||||
|
||||
PaintRoundImageCallback GlobalExclusionListRow::generatePaintUserpicCallback(bool forceRound) {
|
||||
if (const auto peerCached = currentSession()->data().peerLoaded(peerId)) {
|
||||
this->setPeer(peerCached);
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
GlobalExclusionListController::GlobalExclusionListController(not_null<Main::Session *> session,
|
||||
not_null<Window::SessionController*> controller)
|
||||
: _session(session)
|
||||
, _controller(controller)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Main::Session &GlobalExclusionListController::session() const {
|
||||
return *_session;
|
||||
}
|
||||
|
||||
|
||||
void GlobalExclusionListController::prepare() {
|
||||
|
||||
const auto filters = AyuDatabase::getAllRegexFilters();
|
||||
const auto exclusions = AyuDatabase::getAllFiltersExclusions();
|
||||
|
||||
|
||||
if (exclusions.empty()) {
|
||||
auto description = object_ptr<Ui::FlatLabel>(
|
||||
nullptr,
|
||||
tr::ayu_RegexFiltersListEmpty(tr::now),
|
||||
computeListSt().about);
|
||||
delegate()->peerListSetDescription(std::move(description));
|
||||
return;
|
||||
}
|
||||
|
||||
struct FilterCounts {
|
||||
int filters = 0;
|
||||
int exclusions = 0;
|
||||
};
|
||||
std::unordered_map<ID, FilterCounts> countsByDialogIds;
|
||||
|
||||
|
||||
|
||||
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(id));
|
||||
|
||||
auto row = std::make_unique<GlobalExclusionListRow> (peerId);
|
||||
row->setCustomStatus(QString("%1 filters, %2 excluded").arg(count.filters).arg(count.exclusions), false);
|
||||
|
||||
delegate()->peerListAppendRow(reinterpret_cast<std::unique_ptr<PeerListRow> &&>(row));
|
||||
|
||||
}
|
||||
|
||||
// sortByName();
|
||||
|
||||
delegate()->peerListRefreshRows();
|
||||
}
|
||||
|
||||
void GlobalExclusionListController::rowClicked(not_null<PeerListRow*> peer) {
|
||||
_controller->dialogId = peer->id() & PeerId::kChatTypeMask;
|
||||
_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,71 @@
|
|||
// 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 "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 GlobalExclusionListRow final : public PeerListRow
|
||||
{
|
||||
public:
|
||||
explicit GlobalExclusionListRow(PeerId peer);
|
||||
QString generateName() override;
|
||||
PaintRoundImageCallback generatePaintUserpicCallback(bool forceRound) override;
|
||||
|
||||
private:
|
||||
PeerId peerId;
|
||||
};
|
||||
|
||||
class GlobalExclusionListController final : public PeerListController
|
||||
{
|
||||
public:
|
||||
explicit GlobalExclusionListController(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:
|
||||
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,276 @@
|
|||
// 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 "rpl/mappers.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/slide_wrap.h"
|
||||
#include "ui/wrap/vertical_layout.h"
|
||||
#include "window/window_session_controller.h"
|
||||
|
||||
namespace Settings
|
||||
{
|
||||
|
||||
rpl::producer<QString> AyuFiltersList::title()
|
||||
{
|
||||
return tr::ayu_RegexFilters();
|
||||
}
|
||||
|
||||
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::addNewFilter(const RegexFilter &filter, bool exclusion)
|
||||
{
|
||||
|
||||
// stolen from EditPrivacyBox
|
||||
auto state = lifetime().make_state<RegexFilter>(filter);
|
||||
auto buttonText = lifetime().make_state<rpl::variable<QString>>(
|
||||
QString::fromStdString(state->text));
|
||||
auto isRemoved = lifetime().make_state<rpl::variable<bool>>(false);
|
||||
|
||||
auto wrap = _content->add(
|
||||
object_ptr<Ui::SlideWrap<Button>>(
|
||||
_content,
|
||||
object_ptr<Button>(
|
||||
_content,
|
||||
buttonText->value(),
|
||||
st::settingsButtonNoIcon
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const auto button = wrap->entity();
|
||||
|
||||
wrap->toggleOn(isRemoved->value() | rpl::map(!rpl::mappers::_1));
|
||||
|
||||
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, [=](const RegexFilter &done) mutable
|
||||
{
|
||||
buttonText->force_assign(QString::fromStdString(done.text));
|
||||
*state = done;
|
||||
}));
|
||||
}, &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();
|
||||
|
||||
|
||||
if (!state->enabled) {
|
||||
button->setColorOverride(st::storiesComposeGrayText->c);
|
||||
} else {
|
||||
button->setColorOverride({});
|
||||
}
|
||||
button->update();
|
||||
}, state->enabled ? &st::menuIconBlock : &st::menuIconUnblock);
|
||||
|
||||
_contextMenu->addSeparator();
|
||||
|
||||
_contextMenu->addAction(tr::lng_theme_delete(tr::now), [=, this]
|
||||
{
|
||||
AyuDatabase::deleteFilter(state->id);
|
||||
AyuDatabase::deleteExclusionsByFilterId(state->id);
|
||||
FiltersCacheController::rebuildCache();
|
||||
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
|
||||
isRemoved->force_assign(true);
|
||||
|
||||
this->update();
|
||||
|
||||
updateGeometry();
|
||||
repaint();
|
||||
|
||||
// remove headers if there are no more filters left
|
||||
if (filters.empty() && filtersTitle) {
|
||||
filtersTitle->hide();
|
||||
}
|
||||
if (exclusions.empty() && excludedTitle) {
|
||||
excludedTitle->hide();
|
||||
}
|
||||
|
||||
|
||||
// resize();
|
||||
// update();
|
||||
|
||||
}, &st::menuIconDelete);
|
||||
|
||||
_contextMenu->popup(QCursor::pos());
|
||||
};
|
||||
|
||||
// we've opened filters list from top "Exclude" button
|
||||
// on click, close the section
|
||||
auto exclusionsClickHandler = [=, this]() mutable {
|
||||
Expects(dialogId.has_value());
|
||||
|
||||
RegexFilterGlobalExclusion exclusion;
|
||||
exclusion.filterId = state->id;
|
||||
exclusion.dialogId = dialogId.value();
|
||||
|
||||
AyuDatabase::addRegexExclusion(exclusion);
|
||||
FiltersCacheController::rebuildCache();
|
||||
|
||||
AyuSettings::fire_filtersUpdate();
|
||||
|
||||
_controller->showExclude = true;
|
||||
_controller->dialogId = dialogId;
|
||||
|
||||
_controller->showSettings(AyuFiltersList::Id());
|
||||
};
|
||||
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();
|
||||
|
||||
isRemoved->force_assign(true);
|
||||
|
||||
this->update();
|
||||
|
||||
updateGeometry();
|
||||
repaint();
|
||||
|
||||
// remove headers if there are no more filters left
|
||||
if (filters.empty() && filtersTitle) {
|
||||
filtersTitle->hide();
|
||||
}
|
||||
if (exclusions.empty() && excludedTitle) {
|
||||
excludedTitle->hide();
|
||||
}
|
||||
|
||||
}, &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()) {
|
||||
|
||||
filtersTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersHeader());
|
||||
|
||||
for (const auto &filter : filters) {
|
||||
addNewFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (!exclusions.empty()) {
|
||||
excludedTitle = AddSubsectionTitle(container, tr::ayu_RegexFiltersExcluded());
|
||||
|
||||
for (const auto &exclusion : exclusions) {
|
||||
addNewFilter(exclusion, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
void AyuFiltersList::setupContent(not_null<Window::SessionController *> controller)
|
||||
{
|
||||
initializeSharedFilters(_content);
|
||||
|
||||
ResizeFitChild(this, _content);
|
||||
}
|
||||
|
||||
} // namespace Settings
|
|
@ -0,0 +1,52 @@
|
|||
// 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;
|
||||
|
||||
|
||||
|
||||
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,10 @@
|
|||
#include "lang_auto.h"
|
||||
#include "settings_ayu_utils.h"
|
||||
#include "ayu/ayu_settings.h"
|
||||
#include "ayu/features/filters/filters_cache_controller.h"
|
||||
#include "boxes/peer_list_box.h"
|
||||
#include "filters/peer_global_exclusion.h"
|
||||
#include "filters/settings_filters_list.h"
|
||||
#include "settings/settings_common.h"
|
||||
#include "styles/style_settings.h"
|
||||
#include "ui/vertical_list.h"
|
||||
|
@ -242,6 +246,116 @@ void SetupSpyEssentials(not_null<Ui::VerticalLayout*> container) {
|
|||
container->lifetime());
|
||||
}
|
||||
|
||||
void SetupFiltersSettings(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
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();
|
||||
},
|
||||
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();
|
||||
},
|
||||
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();
|
||||
},
|
||||
container->lifetime());
|
||||
|
||||
|
||||
}
|
||||
|
||||
void SetupShared(not_null<Window::SessionController *> controller,
|
||||
Ui::VerticalLayout *container) {
|
||||
Ui::AddSkip(container);
|
||||
|
||||
auto button = container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
rpl::single(QString("Shared Filters"))
|
||||
));
|
||||
button->addClickHandler([=] {
|
||||
controller->dialogId = std::nullopt; // ensure we're handling shared filters
|
||||
controller->showExclude = false;
|
||||
controller->showSettings(AyuFiltersList::Id());
|
||||
});
|
||||
}
|
||||
|
||||
void SetupExclusions(
|
||||
not_null<Window::SessionController*> controller,
|
||||
not_null<Ui::VerticalLayout*> container
|
||||
) {
|
||||
|
||||
container->add(object_ptr<Ui::SettingsButton>(
|
||||
container,
|
||||
rpl::single(QString("Exclusions"))
|
||||
))->addClickHandler([=] {
|
||||
auto ctrl = std::make_unique<GlobalExclusionListController>(
|
||||
&controller->session(),
|
||||
controller
|
||||
);
|
||||
|
||||
auto box = Box<PeerListBox>(std::move(ctrl), [](not_null<PeerListBox*> box) {
|
||||
box->setTitle(rpl::single(QString("Exclusions")));
|
||||
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
|
||||
});
|
||||
|
||||
controller->show(std::move(box));
|
||||
});
|
||||
}
|
||||
void SetupMessageFilters(not_null<Ui::VerticalLayout*> container) {
|
||||
auto *settings = &AyuSettings::getInstance();
|
||||
|
||||
|
@ -327,7 +441,15 @@ void AyuGhost::setupContent(not_null<Window::SessionController*> controller) {
|
|||
AddDivider(content);
|
||||
AddSkip(content);
|
||||
|
||||
SetupMessageFilters(content);
|
||||
|
||||
SetupFiltersSettings(content);
|
||||
|
||||
AddDivider(content);
|
||||
|
||||
SetupShared(controller, content);
|
||||
|
||||
SetupExclusions(controller, content);
|
||||
|
||||
|
||||
AddSkip(content);
|
||||
AddDivider(content);
|
||||
|
|
|
@ -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,44 @@ 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();
|
||||
}
|
||||
|
|
|
@ -61,4 +61,7 @@ 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();
|
|
@ -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,15 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
|||
hasCopyRestrictionForSelected()));
|
||||
}, &st::menuIconTranslate);
|
||||
}
|
||||
if (settings.filtersEnabled) {
|
||||
_menu->addAction(tr::ayu_RegexFilterQuickAdd(tr::now), [=] {
|
||||
RegexFilter filter;
|
||||
filter.text = selectedText.rich.text.toStdString();
|
||||
auto dialogId = static_cast<long long>(item->history()->peer->id.value & PeerId::kChatTypeMask);
|
||||
|
||||
_controller->show(Settings::RegexEditBox(&filter, {}, dialogId, true));
|
||||
}, &st::menuIconAddToFolder);
|
||||
}
|
||||
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)) {
|
||||
|
|
|
@ -661,7 +661,11 @@ HistoryWidget::HistoryWidget(
|
|||
AyuSettings::get_hideFromBlockedReactive() | rpl::to_empty,
|
||||
session().changes().peerUpdates(
|
||||
Data::PeerUpdate::Flag::IsBlocked
|
||||
) | rpl::to_empty
|
||||
) | rpl::to_empty,
|
||||
AyuSettings::get_filtersUpdate() | rpl::to_empty,
|
||||
AyuSettings::get_filtersEnabledReactive() | rpl::to_empty,
|
||||
AyuSettings::get_filtersEnabledInChatsReactive() | rpl::to_empty,
|
||||
AyuSettings::get_shadowBanIdsReactive() | rpl::to_empty
|
||||
) | rpl::start_with_next(
|
||||
[=]
|
||||
{
|
||||
|
|
|
@ -1592,7 +1592,7 @@ void Element::destroyUnreadBar() {
|
|||
}
|
||||
|
||||
int Element::displayedDateHeight() const {
|
||||
if (AyuFeatures::MessageShot::isTakingShot()) {
|
||||
if (AyuFeatures::MessageShot::isTakingShot() || isMessageHidden(data())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,7 +53,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||
#include "styles/style_menu_icons.h"
|
||||
#include "styles/style_layers.h"
|
||||
|
||||
// AyuGram includes
|
||||
#include <styles/style_ayu_icons.h>
|
||||
#include <styles/style_settings.h>
|
||||
|
||||
#include "ayu/ui/settings/settings_ayu.h"
|
||||
#include "ayu/ui/settings/filters/edit_filter.h"
|
||||
#include "ayu/ui/settings/filters/settings_filters_list.h"
|
||||
#include "info/info_memento.h"
|
||||
namespace Info {
|
||||
namespace {
|
||||
|
||||
|
@ -410,6 +417,58 @@ void WrapWidget::setupTopBarMenuToggle() {
|
|||
Box(Ui::FillPeerQrBox, self, std::nullopt, nullptr));
|
||||
});
|
||||
}
|
||||
} else if (section.settingsType() == ::Settings::AyuFiltersList::Id()) {
|
||||
const auto controller = _controller->parentController();
|
||||
if (true) {
|
||||
const auto &st = st::ayuFiltersAddIcon;
|
||||
const auto button = _topBar->addButton(
|
||||
base::make_unique_q<Ui::IconButton>(_topBar, st));
|
||||
|
||||
const auto show = controller->uiShow();
|
||||
|
||||
button->addClickHandler([=, content = _content.data()]
|
||||
{
|
||||
const auto onDone = [=](const RegexFilter &)
|
||||
{
|
||||
// taken from WrapWidget::createMemento
|
||||
|
||||
// I cant neither update nor redraw filters list (skill issue)
|
||||
// so just close current section and
|
||||
// open new with the same memento
|
||||
auto contentMemento = content->createMemento();
|
||||
if (!contentMemento) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<ContentMemento>> stack;
|
||||
stack.push_back(std::move(contentMemento));
|
||||
|
||||
auto sectionMemento = std::make_shared<Memento>(std::move(stack));
|
||||
|
||||
// close the last section
|
||||
showBackFromStackInternal(Window::SectionShow(Window::SectionShow::Way::Backward, anim::type::normal));
|
||||
// open new section
|
||||
showInternal(gsl::make_not_null(sectionMemento.get()), Window::SectionShow(anim::type::normal));
|
||||
};
|
||||
show->show(::Settings::RegexEditBox(nullptr, onDone, controller->dialogId));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (controller->showExclude.has_value() && controller->showExclude.value()) {
|
||||
auto icon = base::make_unique_q<Ui::IconButton>(_topBar, st::ayuFiltersExcludeIcon);
|
||||
|
||||
const auto excludeButton = _topBar->addButton(std::move(icon));
|
||||
excludeButton->addClickHandler([=, content = _content.data()]
|
||||
{
|
||||
// close current filters list
|
||||
showBackFromStackInternal(Window::SectionShow(Window::SectionShow::Way::Backward, anim::type::normal));
|
||||
|
||||
// open new
|
||||
controller->showExclude = false;
|
||||
controller->showSettings(::Settings::AyuFiltersList::Id());
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (key.storiesPeer()
|
||||
&& key.storiesPeer()->isSelf()
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue