diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java
index 3d608124e0..82539fa361 100644
--- a/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/MessageView.java
@@ -606,6 +606,14 @@ public static Object fillMessageOptions (MessagesController m, TGMessage msg, @N
boolean isSent = !msg.isNotSent();
Object tag = null;
+ final boolean isHiddenByMessagesFilter = msg.isHiddenByMessagesFilter();
+
+ if (!isMore && (isHiddenByMessagesFilter /*|| BuildConfig.DEBUG*/)) {
+ ids.append(R.id.btn_messageChangeMessageFilterVisibility);
+ strings.append(Lang.getString(R.string.MessagesFilterShowMessage));
+ icons.append(R.drawable.baseline_visibility_24);
+ }
+
// Promotion
if (msg.isSponsoredMessage()) {
@@ -1502,7 +1510,7 @@ public boolean onTouchEvent (MotionEvent e) {
} else {
preventLongPress();
}
- if (c.inSelectMode() || !msg.onTouchEvent(this, e)) {
+ if (c.inSelectMode() || msg.isHiddenByMessagesFilter() || !msg.onTouchEvent(this, e)) {
flags |= FLAG_CAUGHT_CLICK;
} else {
flags |= FLAG_CAUGHT_MESSAGE_TOUCH;
diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/filter/Content.java b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/Content.java
new file mode 100644
index 0000000000..6640fc58a6
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/Content.java
@@ -0,0 +1,74 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 17/11/2023
+ */
+package org.thunderdog.challegram.component.chat.filter;
+
+import android.net.Uri;
+
+import org.drinkless.tdlib.TdApi;
+import org.thunderdog.challegram.data.TD;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import me.vkryl.td.ChatId;
+import me.vkryl.td.Td;
+
+class Content {
+ public final Set mentionsUsername = new HashSet<>();
+ public final Set mentionsId = new HashSet<>();
+ public final Set internalLinks = new HashSet<>();
+ public final Set externalLinks = new HashSet<>();
+
+ public Content () {}
+
+ public void add (TdApi.MessageContent content) {
+ final TdApi.FormattedText textOrCaption = Td.textOrCaption(content);
+ if (textOrCaption == null || textOrCaption.text == null) return;
+
+ if (textOrCaption.entities != null) {
+ for (TdApi.TextEntity entity : textOrCaption.entities) {
+ switch (entity.type.getConstructor()) {
+ case TdApi.TextEntityTypeMention.CONSTRUCTOR: {
+ mentionsUsername.add(Td.substring(textOrCaption.text, entity));
+ break;
+ }
+ case TdApi.TextEntityTypeMentionName.CONSTRUCTOR: {
+ mentionsId.add(ChatId.fromUserId(((TdApi.TextEntityTypeMentionName) entity.type).userId));
+ break;
+ }
+ case TdApi.TextEntityTypeUrl.CONSTRUCTOR: {
+ addLink(Td.substring(textOrCaption.text, entity));
+ break;
+ }
+ case TdApi.TextEntityTypeTextUrl.CONSTRUCTOR: {
+ addLink(((TdApi.TextEntityTypeTextUrl) entity.type).url);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private void addLink (String link) {
+ if (TD.isTelegramOwnedHost(Uri.parse(link), false)) {
+ internalLinks.add(link);
+ } else {
+ externalLinks.add(link);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterReason.java b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterReason.java
new file mode 100644
index 0000000000..97ae9808df
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterReason.java
@@ -0,0 +1,39 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.component.chat.filter;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ FilterReason.PENDING,
+ FilterReason.NONE,
+ FilterReason.BLOCKED_SENDER,
+ FilterReason.BLOCKED_SENDER_MENTION,
+ FilterReason.CONTAINS_INTERNAL_LINK,
+ FilterReason.CONTAINS_EXTERNAL_LINK
+})
+public @interface FilterReason {
+ int
+ PENDING = -1,
+ NONE = 0,
+ BLOCKED_SENDER = 1,
+ BLOCKED_SENDER_MENTION = 2,
+ CONTAINS_INTERNAL_LINK = 3,
+ CONTAINS_EXTERNAL_LINK = 4;
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterState.java b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterState.java
new file mode 100644
index 0000000000..7d6610dfb2
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/FilterState.java
@@ -0,0 +1,35 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.component.chat.filter;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ FilterState.VISIBLE,
+ FilterState.HIDDEN,
+ FilterState.LAST_STATE_VISIBLE,
+ FilterState.LAST_STATE_HIDDEN
+})
+public @interface FilterState {
+ int
+ VISIBLE = 0,
+ HIDDEN = 1,
+ LAST_STATE_VISIBLE = 2,
+ LAST_STATE_HIDDEN = 3;
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessageFilterProcessingState.java b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessageFilterProcessingState.java
new file mode 100644
index 0000000000..e860932f2f
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessageFilterProcessingState.java
@@ -0,0 +1,389 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.component.chat.filter;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+
+import org.drinkless.tdlib.TdApi;
+import org.thunderdog.challegram.data.TGMessage;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.tool.UI;
+import org.thunderdog.challegram.unsorted.Settings;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import me.vkryl.core.collection.LongSparseIntArray;
+import me.vkryl.core.lambda.CancellableRunnable;
+import me.vkryl.core.lambda.Destroyable;
+import me.vkryl.td.Td;
+
+public class MessageFilterProcessingState implements Destroyable, MessagesFilterProvider.MessageSenderUpdatesListener,
+ MessagesFilterProvider.UsernameResolverUpdatesListener, MessagesFilterProvider.ChatCustomFilterSettingsUpdatesListener {
+ private final Tdlib tdlib;
+ private final TGMessage message;
+
+ private final long chatId;
+ private final long senderId;
+
+ private final Set resolvedMentions = new HashSet<>();
+ private boolean senderIsBlocked;
+
+
+ public MessageFilterProcessingState (TGMessage tgMessage, TdApi.Message message) {
+ this.tdlib = tgMessage.tdlib();
+ this.message = tgMessage;
+
+ this.chatId = message.chatId;
+ this.senderId = Td.getSenderId(message.senderId);
+ this.senderIsBlocked = tdlib.chatFullyBlocked(senderId);
+
+ this.rawContents.put(message.id, message.content);
+ checkContent();
+
+ subscribeToSenderId(senderId);
+ tdlib.messagesFilterProvider().subscribeToChatCustomFilterSettingsUpdates(chatId, this);
+ }
+
+ @Override
+ public void onChatFilterSettingsUpdate (long chatId) {
+ if (this.chatId == chatId) {
+ checkCurrentFilterReason();
+ }
+ }
+
+ @Override
+ public void onMessageSenderBlockedUpdate (long chatId, boolean isBlocked) {
+ boolean needCheckReason = false;
+
+ if (this.senderId == chatId) {
+ this.senderIsBlocked = isBlocked;
+ needCheckReason = true;
+ }
+ if (this.replySenderId == chatId) {
+ this.replySenderIsBlocked = isBlocked;
+ needCheckReason = true;
+ }
+ if (messageContent != null && messageContent.mentionsId.contains(chatId)) {
+ needCheckReason = true;
+ }
+ if (resolvedMentions.contains(chatId)) {
+ needCheckReason = true;
+ }
+ if (needCheckReason) {
+ checkCurrentFilterReason();
+ }
+ }
+
+ @Override
+ public void onUsernameResolverUpdate (String username, long chatId) {
+ if (messageContent != null && messageContent.mentionsUsername.contains(username)) {
+ if (resolvedMentions.add(chatId)) {
+ subscribeToSenderId(chatId);
+ checkCurrentFilterReason();
+ }
+ }
+ }
+
+ private void checkCurrentFilterReason () {
+ if (Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS)) {
+ if (senderIsBlocked) {
+ setReason(FilterReason.BLOCKED_SENDER);
+ return;
+ }
+ }
+
+ if (Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS_MENTIONS)) {
+ if (replySenderIsBlocked) {
+ setReason(FilterReason.BLOCKED_SENDER_MENTION);
+ return;
+ }
+
+ if (messageContent != null) {
+ for (Long chatId : messageContent.mentionsId) {
+ if (tdlib.chatFullyBlocked(chatId)) {
+ setReason(FilterReason.BLOCKED_SENDER_MENTION);
+ return;
+ }
+ }
+ }
+
+ for (Long chatId : resolvedMentions) {
+ if (tdlib.chatFullyBlocked(chatId)) {
+ setReason(FilterReason.BLOCKED_SENDER_MENTION);
+ return;
+ }
+ }
+ }
+
+ if (messageContent != null) {
+ if (Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_EXTERNAL)) {
+ if (!messageContent.externalLinks.isEmpty()) {
+ setReason(FilterReason.CONTAINS_EXTERNAL_LINK);
+ return;
+ }
+ }
+ if (Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_INTERNAL)) {
+ if (!messageContent.internalLinks.isEmpty()) {
+ setReason(FilterReason.CONTAINS_INTERNAL_LINK);
+ return;
+ }
+ if (!messageContent.mentionsUsername.isEmpty()) { // todo: ignore self-mention
+ setReason(FilterReason.CONTAINS_INTERNAL_LINK);
+ return;
+ }
+ }
+ }
+
+ setReason(FilterReason.NONE);
+ }
+
+
+
+ private long replySenderId;
+ private boolean replySenderIsBlocked;
+
+ public void setReplySenderId (@Nullable TdApi.MessageSender sender) {
+ long senderId = Td.getSenderId(sender);
+ if (replySenderId == senderId) return;
+
+ if (replySenderId != 0) {
+ unsubscribeFromSenderId(replySenderId);
+ }
+
+ subscribeToSenderId(senderId);
+ this.replySenderId = senderId;
+ this.replySenderIsBlocked = tdlib.chatFullyBlocked(senderId);
+ checkCurrentFilterReason();
+ }
+
+
+
+
+
+
+
+
+
+
+ /* Combine and update content logic */
+
+ private Content messageContent;
+
+ final HashMap rawContents = new HashMap<>();
+ private CancellableRunnable checkContentRunnable;
+
+ @UiThread
+ public void updateMessageContent (long messageId, TdApi.MessageContent content) {
+ rawContents.put(messageId, content);
+ checkContent();
+ }
+
+ @AnyThread
+ public void combineWith (TdApi.Message message) {
+ if (!UI.inUiThread()) {
+ UI.post(() -> combineWith(message));
+ return;
+ }
+
+
+ rawContents.put(message.id, message.content);
+ if (checkContentRunnable != null) {
+ checkContentRunnable.cancel();
+ }
+
+ UI.post(checkContentRunnable = new CancellableRunnable() {
+ @Override
+ public void act () {
+ checkContentRunnable = null;
+ checkContent();
+ }
+ });
+ }
+
+ private void checkContent () {
+ if (messageContent != null) {
+ unsubscribeFromContent(messageContent);
+ }
+
+ this.messageContent = new Content();
+ for (Map.Entry entry: rawContents.entrySet()) {
+ this.messageContent.add(entry.getValue());
+ }
+
+ subscribeToContent(messageContent);
+ checkCurrentFilterReason();
+ }
+
+
+
+ /* Subscriptions controls */
+
+ private void subscribeToContent (Content content) {
+ for (Long chatId : content.mentionsId) {
+ subscribeToSenderId(chatId);
+ }
+
+ resolvedMentions.clear();
+ for (String username : content.mentionsUsername) {
+ subscribeToUsernameResolver(username);
+ long chatId = tdlib.messagesFilterProvider().resolveUsername(username);
+ if (chatId != 0) {
+ if (resolvedMentions.add(chatId)) {
+ subscribeToSenderId(chatId);
+ }
+ }
+ }
+ }
+
+ private void unsubscribeFromContent (Content content) {
+ for (Long chatId : content.mentionsId) {
+ unsubscribeFromSenderId(chatId);
+ }
+ for (String username : content.mentionsUsername) {
+ unsubscribeFromUsernameResolver(username);
+ }
+ for (Long chatId : resolvedMentions) {
+ unsubscribeFromSenderId(chatId);
+ }
+ resolvedMentions.clear();
+ }
+
+
+ private final LongSparseIntArray senderSubscriptions = new LongSparseIntArray();
+
+ private void subscribeToSenderId (long senderId) {
+ int count = senderSubscriptions.get(senderId, -1);
+ if (count == -1) {
+ tdlib.messagesFilterProvider().subscribeToMessageSenderUpdates(senderId, this);
+ senderSubscriptions.put(senderId, 1);
+ } else {
+ senderSubscriptions.put(senderId, count + 1);
+ }
+ }
+
+ private void unsubscribeFromSenderId (long senderId) {
+ int count = senderSubscriptions.get(senderId, -1);
+ if (count <= 1) {
+ tdlib.messagesFilterProvider().unsubscribeFromMessageSenderUpdates(senderId, this);
+ senderSubscriptions.delete(senderId);
+ } else {
+ senderSubscriptions.put(senderId, count - 1);
+ }
+ }
+
+ private void unsubscribeFromAllSenderIds () {
+ int s = senderSubscriptions.size();
+ for (int a = 0; a < s; a++) {
+ tdlib.messagesFilterProvider().unsubscribeFromMessageSenderUpdates(senderSubscriptions.keyAt(a), this);
+ }
+ senderSubscriptions.clear();
+ }
+
+
+ private final HashMap usernameResolverSubscriptions = new HashMap<>();
+
+ private void subscribeToUsernameResolver (String username) {
+ Integer count = usernameResolverSubscriptions.get(username);
+ if (count == null) {
+ tdlib.messagesFilterProvider().subscribeToUsernameResolverUpdates(username, this);
+ usernameResolverSubscriptions.put(username, 1);
+ } else {
+ usernameResolverSubscriptions.put(username, count + 1);
+ }
+ }
+
+ private void unsubscribeFromUsernameResolver (String username) {
+ Integer count = usernameResolverSubscriptions.get(username);
+ if (count == null) return;
+
+ if (count <= 1) {
+ tdlib.messagesFilterProvider().unsubscribeFromUsernameResolverUpdates(username, this);
+ usernameResolverSubscriptions.remove(username);
+ } else {
+ usernameResolverSubscriptions.put(username, count - 1);
+ }
+ }
+
+ private void unsubscribeFromAllFromUsernameResolvers () {
+ for (Map.Entry entry : usernameResolverSubscriptions.entrySet()) {
+ tdlib.messagesFilterProvider().unsubscribeFromUsernameResolverUpdates(entry.getKey(), this);
+ }
+ usernameResolverSubscriptions.clear();
+ }
+
+
+
+ /* State */
+
+ private @FilterState int state = FilterState.LAST_STATE_VISIBLE;
+ private @FilterReason int reason = FilterReason.PENDING;
+
+ private void setReason (@FilterReason int reason) {
+ if (this.reason == reason) return;
+ this.reason = reason;
+
+ if (reason == FilterReason.NONE) {
+ setState(FilterState.VISIBLE);
+ } else if (reason == FilterReason.PENDING) {
+ if (state == FilterState.VISIBLE) {
+ setState(FilterState.LAST_STATE_VISIBLE);
+ } else if (state == FilterState.HIDDEN) {
+ setState(FilterState.LAST_STATE_HIDDEN);
+ }
+ } else {
+ setState(FilterState.HIDDEN);
+ }
+ }
+
+ private void setState (@FilterState int state) {
+ final boolean oldNeedHideMessage = needHideMessage();
+
+ if (this.state == state) return;
+ this.state = state;
+
+ final boolean newNeedHideMessage = needHideMessage();
+ if (newNeedHideMessage != oldNeedHideMessage) {
+ message.setIsHiddenByMessagesFilter(newNeedHideMessage, true);
+ }
+ }
+
+ @FilterReason
+ public int getCurrentFilterReason () {
+ return reason;
+ }
+
+ public boolean needHideMessage () {
+ return state == FilterState.LAST_STATE_HIDDEN || state == FilterState.HIDDEN;
+ }
+
+
+
+ /* * */
+
+ @Override
+ public void performDestroy () {
+ if (messageContent != null) {
+ unsubscribeFromContent(messageContent);
+ }
+ unsubscribeFromAllSenderIds();
+ unsubscribeFromAllFromUsernameResolvers();
+ tdlib.messagesFilterProvider().unsubscribeFromChatCustomFilterSettingsUpdates(chatId, this);
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessagesFilterProvider.java b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessagesFilterProvider.java
new file mode 100644
index 0000000000..41999b71b5
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/component/chat/filter/MessagesFilterProvider.java
@@ -0,0 +1,168 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.component.chat.filter;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import org.drinkless.tdlib.TdApi;
+import org.thunderdog.challegram.telegram.ChatListener;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.tool.UI;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+import me.vkryl.core.reference.ReferenceLongMap;
+import me.vkryl.core.reference.ReferenceMap;
+
+public class MessagesFilterProvider implements ChatListener {
+
+ private final Tdlib tdlib;
+
+ public MessagesFilterProvider (Tdlib tdlib) {
+ this.tdlib = tdlib;
+
+ // memoryLeakDebug();
+ }
+
+
+ /**/
+
+ public interface ChatCustomFilterSettingsUpdatesListener {
+ void onChatFilterSettingsUpdate (long chatId);
+ }
+
+ private final ReferenceLongMap chatCustomFilterSettingsUpdatesListeners = new ReferenceLongMap<>();
+
+ public void subscribeToChatCustomFilterSettingsUpdates (long chatId, ChatCustomFilterSettingsUpdatesListener listener) {
+ chatCustomFilterSettingsUpdatesListeners.add(chatId, listener);
+ }
+
+ public void unsubscribeFromChatCustomFilterSettingsUpdates (long chatId, ChatCustomFilterSettingsUpdatesListener listener) {
+ chatCustomFilterSettingsUpdatesListeners.remove(chatId, listener);
+ }
+
+ public void updateChatCustomFilterSettings (long chatId) {
+ updateChatCustomFilterSettings(chatId, chatCustomFilterSettingsUpdatesListeners.iterator(chatId));
+ }
+
+ private static void updateChatCustomFilterSettings (long chatId, @Nullable Iterator list) {
+ if (list != null) {
+ while (list.hasNext()) {
+ list.next().onChatFilterSettingsUpdate(chatId);
+ }
+ }
+ }
+
+
+
+ /* * */
+
+ public interface MessageSenderUpdatesListener {
+ void onMessageSenderBlockedUpdate (long chatId, boolean isBlocked);
+ }
+
+ private final ReferenceLongMap senderUpdateListeners = new ReferenceLongMap<>();
+
+ public void subscribeToMessageSenderUpdates (long chatId, MessageSenderUpdatesListener listener) {
+ if (!senderUpdateListeners.has(chatId)) {
+ tdlib.listeners().subscribeToChatUpdates(chatId, this);
+ }
+
+ senderUpdateListeners.add(chatId, listener);
+ }
+
+ public void unsubscribeFromMessageSenderUpdates (long chatId, MessageSenderUpdatesListener listener) {
+ senderUpdateListeners.remove(chatId, listener);
+ if (!senderUpdateListeners.has(chatId)) {
+ tdlib.listeners().unsubscribeFromChatUpdates(chatId, this);
+ }
+ }
+
+ @Override
+ public void onChatBlockListChanged (long chatId, @Nullable TdApi.BlockList blockList) {
+ UI.post(() -> updateChatUnreadMentionCount(chatId, tdlib.chatFullyBlocked(chatId), senderUpdateListeners.iterator(chatId)));
+ }
+
+ private static void updateChatUnreadMentionCount (long chatId, boolean isBlocked, @Nullable Iterator list) {
+ if (list != null) {
+ while (list.hasNext()) {
+ list.next().onMessageSenderBlockedUpdate(chatId, isBlocked);
+ }
+ }
+ }
+
+
+
+ /* * */
+
+ public interface UsernameResolverUpdatesListener {
+ void onUsernameResolverUpdate (String username, long chatId);
+ }
+
+ private final HashMap usernamesCache = new HashMap<>(); // Fixme: Username owner may change
+ private final ReferenceMap usernameResolverCallbacks = new ReferenceMap<>();
+
+ public long resolveUsername (String username) {
+ Long cachedChatId = usernamesCache.get(username);
+ if (cachedChatId != null) {
+ return cachedChatId;
+ }
+
+ if (!usernamesCache.containsKey(username)) {
+ usernamesCache.put(username, null);
+ tdlib.send(new TdApi.SearchPublicChat(username), (TdApi.Chat chat, TdApi.Error error) -> {
+ if (error != null) return;
+ UI.post(() -> {
+ usernamesCache.put(username, chat.id);
+ updateUsernameResolverResult(username, chat.id, usernameResolverCallbacks.iterator(username));
+ });
+ });
+
+ }
+ return 0;
+ }
+
+ public void subscribeToUsernameResolverUpdates (String username, UsernameResolverUpdatesListener listener) {
+ usernameResolverCallbacks.add(username, listener);
+ }
+
+ public void unsubscribeFromUsernameResolverUpdates (String username, UsernameResolverUpdatesListener listener) {
+ usernameResolverCallbacks.remove(username, listener);
+ }
+
+ private static void updateUsernameResolverResult (String username, long chatId, @Nullable Iterator list) {
+ if (list != null) {
+ while (list.hasNext()) {
+ list.next().onUsernameResolverUpdate(username, chatId);
+ }
+ }
+ }
+
+
+
+ private void memoryLeakDebug () {
+ final Set> set1 = senderUpdateListeners.keySetUnchecked();
+ final Set> set2 = usernameResolverCallbacks.keySetUnchecked();
+
+ int size = (set1 != null ? set1.size() : 0) + (set2 != null ? set2.size() : 0);
+
+ Log.i("FILTER_MEM_LEAK_DEBUG", "" + size);
+ UI.post(this::memoryLeakDebug, 1000);
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/data/TD.java b/app/src/main/java/org/thunderdog/challegram/data/TD.java
index cc4fe72d55..ea658037bb 100644
--- a/app/src/main/java/org/thunderdog/challegram/data/TD.java
+++ b/app/src/main/java/org/thunderdog/challegram/data/TD.java
@@ -5422,4 +5422,30 @@ public static int chatTypeAccentColorId (@IdRes int chatType) {
}
public static final String[] ICON_NAMES = {"All", "Unread", "Unmuted", "Bots", "Channels", "Groups", "Private", "Custom", "Setup", "Cat", "Crown", "Favorite", "Flower", "Game", "Home", "Love", "Mask", "Party", "Sport", "Study", "Trade", "Travel", "Work", "Airplane", "Book", "Light", "Like", "Money", "Note", "Palette"};
+
+ public static boolean isTelegramOwnedHost (Uri uri, boolean allowTelegraph) {
+ String host = uri.getHost();
+ if (host == null) {
+ return false;
+ }
+
+ for (String knownHost : TdConstants.TME_HOSTS) {
+ if (StringUtils.equalsOrBothEmpty(host, knownHost) || host.endsWith("." + knownHost)) {
+ return true;
+ }
+ }
+ if (allowTelegraph) {
+ for (String knownHost : TdConstants.TELEGRAM_HOSTS) {
+ if (StringUtils.equalsOrBothEmpty(host, knownHost) || host.endsWith("." + knownHost)) {
+ return true;
+ }
+ }
+ for (String knownHost : TdConstants.TELEGRAPH_HOSTS) {
+ if (StringUtils.equalsOrBothEmpty(host, knownHost) || host.endsWith("." + knownHost)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java
index acac48f224..1c0b6a0428 100644
--- a/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java
+++ b/app/src/main/java/org/thunderdog/challegram/data/TGMessage.java
@@ -63,6 +63,7 @@
import org.thunderdog.challegram.component.chat.MessageViewGroup;
import org.thunderdog.challegram.component.chat.MessagesManager;
import org.thunderdog.challegram.component.chat.ReplyComponent;
+import org.thunderdog.challegram.component.chat.filter.MessageFilterProcessingState;
import org.thunderdog.challegram.component.sticker.TGStickerObj;
import org.thunderdog.challegram.config.Config;
import org.thunderdog.challegram.config.Device;
@@ -303,6 +304,7 @@ public void onFactorChanged (int id, float factor, float fraction, FactorAnimato
public static final int REACTIONS_DRAW_MODE_ONLY_ICON = 2;
private final TranslationsManager mTranslationsManager;
+ private final @Nullable MessageFilterProcessingState mMessageFilterProcessingState;
protected TGMessage (MessagesManager manager, TdApi.Message msg) {
this(manager, msg, null);
@@ -531,6 +533,20 @@ public void onInvalidateReceiversRequested () {
checkHighlightedText();
UI.post(() -> updateReactionAvatars(false));
+
+ this.isHiddenByFilter = new BoolAnimator(IS_HIDDEN_BY_MESSAGE_FILTER_ANIMATOR_ID, (a, b, c, d) -> {
+ if (BitwiseUtils.hasFlag(flags, FLAG_LAYOUT_BUILT)) {
+ notifyBubbleChanged();
+ invalidate();
+ }
+ }, AnimatorUtils.DECELERATE_INTERPOLATOR, 320L);
+
+ final boolean needUseMessagesFilter = !tdlib.isUserChat(msg.chatId)
+ && tdlib.myUserId() != Td.getSenderId(msg.senderId)
+ && Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_ENABLED);
+
+ this.mMessageFilterProcessingState = needUseMessagesFilter ?
+ new MessageFilterProcessingState(this, msg) : null;
}
private static @NonNull T nonNull (@Nullable T value) {
@@ -894,6 +910,26 @@ protected final boolean useForward () {
return msg.forwardInfo != null && (!useBubbles() || !separateReplyFromBubble()) && !forceForwardOrImportInfo();
}
+
+
+ //
+
+ private static final int IS_HIDDEN_BY_MESSAGE_FILTER_ANIMATOR_ID = 2;
+
+ private static final int HIDDEN_BY_MESSAGE_FILTER_HEIGHT = 35;
+
+ private final BoolAnimator isHiddenByFilter;
+
+ public void setIsHiddenByMessagesFilter (boolean hidden, boolean animated) {
+ isHiddenByFilter.setValue(hidden && !isSponsoredMessage(), BitwiseUtils.hasFlag(flags, FLAG_LAYOUT_BUILT) && currentViews.hasAnyTargetToInvalidate() && UI.inUiThread() && controller() != null && controller().isFocused() && animated);
+ }
+
+ public boolean isHiddenByMessagesFilter () {
+ return isHiddenByFilter.getValue();
+ }
+
+
+
private static final int VIEW_COUNT_HIDDEN = 0;
private static final int VIEW_COUNT_MAIN = 1;
private static final int VIEW_COUNT_FORWARD = 2;
@@ -1317,8 +1353,10 @@ protected final int getExtraPadding () {
}
public int computeHeight () {
+ final int headerPadding = getHeaderPadding();
+ final int extraPadding = getExtraPadding();
if (useBubbles()) {
- int height = bottomContentEdge + getPaddingBottom() + getExtraPadding();
+ int height = bottomContentEdge + getPaddingBottom() + extraPadding;
if (inlineKeyboard != null && !inlineKeyboard.isEmpty()) {
height += inlineKeyboard.getHeight() + TGInlineKeyboard.getButtonSpacing();
}
@@ -1332,9 +1370,9 @@ public int computeHeight () {
if (commentButton.isBubble()) {
height += commentButton.getAnimatedHeight(Screen.dp(5f), commentButton.getVisibility());
}
- return height;
+ return MathUtils.fromTo(height, Screen.dp(HIDDEN_BY_MESSAGE_FILTER_HEIGHT) + extraPadding + headerPadding, isHiddenByFilter.getFloatValue());
} else {
- int height = pContentY + getContentHeight() + getPaddingBottom() + getExtraPadding();
+ int height = pContentY + getContentHeight() + getPaddingBottom() + extraPadding;
if (inlineKeyboard != null && !inlineKeyboard.isEmpty()) {
height += inlineKeyboard.getHeight() + xPaddingBottom;
}
@@ -1348,7 +1386,7 @@ public int computeHeight () {
if (commentButton.isVisible() && commentButton.isInline()) {
height += commentButton.getAnimatedHeight(useReactionBubbles ? -Screen.dp(2f) : 0, commentButton.getVisibility());
}
- return height;
+ return MathUtils.fromTo(height, Screen.dp(HIDDEN_BY_MESSAGE_FILTER_HEIGHT) + extraPadding + headerPadding, isHiddenByFilter.getFloatValue());
}
}
@@ -1880,6 +1918,12 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat
checkEdges();
+ final float isHiddenFactor = isHiddenByFilter.getFloatValue();
+ if (isHiddenFactor == 1f) {
+ drawHiddenMessage(view, c, isHiddenFactor);
+ return;
+ }
+
// "Unread messages" / "Discussion started" badge
if ((flags & FLAG_SHOW_BADGE) != 0) {
int top = 0;
@@ -2367,6 +2411,19 @@ public final void draw (MessageView view, Canvas c, @NonNull AvatarReceiver avat
startSetReactionAnimationIfReady();
highlightUnreadReactionsIfNeeded();
+ if (isHiddenFactor > 0f) {
+ drawHiddenMessage(view, c, isHiddenFactor);
+ }
+ }
+
+ public final void drawHiddenMessage (MessageView view, Canvas c, float isHiddenFactor) {
+ final int viewWidth = view.getMeasuredWidth();
+ final int viewHeight = view.getMeasuredHeight();
+ final int y = getHeaderPadding();
+
+ c.drawRect(0, y, viewWidth, viewHeight, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.filling))));
+ // c.drawRect(0, y, viewWidth, y + 1, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.separator))));
+ c.drawRect(0, viewHeight - 1, viewWidth, viewHeight, Paints.fillingPaint(ColorUtils.alphaColor(isHiddenFactor, Theme.getColor(ColorId.separator))));
}
protected final boolean needColoredNames () {
@@ -5177,6 +5234,9 @@ public int replaceMessageContent (long chatId, long messageId, TdApi.MessageCont
if (message == null) {
return MESSAGE_NOT_CHANGED;
}
+ if (mMessageFilterProcessingState != null) {
+ mMessageFilterProcessingState.updateMessageContent(messageId, newContent);
+ }
if ((flags & FLAG_UNSUPPORTED) != 0) {
if (message.content.getConstructor() == TdApi.MessageUnsupported.CONSTRUCTOR && newContent.getConstructor() != TdApi.MessageUnsupported.CONSTRUCTOR) {
message.content = newContent;
@@ -5938,6 +5998,9 @@ public final void onDestroy () {
if (replyData != null)
replyData.performDestroy();
messageReactions.performDestroy();
+ if (mMessageFilterProcessingState != null) {
+ mMessageFilterProcessingState.performDestroy();
+ }
setViewAttached(false);
onMessageContainerDestroyed();
}
diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java
index 7a7bab813f..b090166c28 100644
--- a/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java
+++ b/app/src/main/java/org/thunderdog/challegram/telegram/Tdlib.java
@@ -44,6 +44,7 @@
import org.thunderdog.challegram.TDLib;
import org.thunderdog.challegram.U;
import org.thunderdog.challegram.component.chat.TdlibSingleUnreadReactionsManager;
+import org.thunderdog.challegram.component.chat.filter.MessagesFilterProvider;
import org.thunderdog.challegram.component.dialogs.ChatView;
import org.thunderdog.challegram.config.Config;
import org.thunderdog.challegram.core.Lang;
@@ -453,6 +454,7 @@ public long timeWasted () {
private final TdlibFileGenerationManager fileGenerationManager;
private final TdlibSingleUnreadReactionsManager unreadReactionsManager;
private final TdlibMessageViewer messageViewer;
+ private final MessagesFilterProvider messagesFilterProvider;
private final HashSet channels = new HashSet<>();
private final LongSparseLongArray accessibleChatTimers = new LongSparseLongArray();
@@ -660,6 +662,7 @@ public TdlibCounter getCounter (@NonNull TdApi.ChatList chatList) {
Log.v("INITIALIZATION: Tdlib.messageViewer -> %dms", SystemClock.uptimeMillis() - ms);
ms = SystemClock.uptimeMillis();
}
+ this.messagesFilterProvider = new MessagesFilterProvider(this);
this.unreadReactionsManager = new TdlibSingleUnreadReactionsManager(this);
this.applicationConfigJson = settings().getApplicationConfig();
if (!StringUtils.isEmpty(applicationConfigJson)) {
@@ -2326,6 +2329,10 @@ public TdlibListeners listeners () {
return listeners;
}
+ public MessagesFilterProvider messagesFilterProvider() {
+ return messagesFilterProvider;
+ }
+
public TdlibStatusManager status () {
return statusManager;
}
diff --git a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java
index 3f5372cec9..a42e86781e 100644
--- a/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java
+++ b/app/src/main/java/org/thunderdog/challegram/telegram/TdlibUi.java
@@ -7014,4 +7014,21 @@ public void onScrollStateChanged (@NonNull RecyclerView recyclerView, int newSta
recyclerView.addOnScrollListener(onScrollListener);
return viewMessages;
}
+
+ public void openMessageFilterSettings (ViewController> context, long chatId, @Nullable Runnable after) {
+ context.showSettings(R.id.btn_chatMessageLinksFilter, new ListItem[] {
+ new ListItem(ListItem.TYPE_INFO, 0, 0, R.string.ChatMessagesFilterInfo),
+ new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_chatMessageLinksFilterInternal, 0, R.string.ChatMessagesFilterInternal, R.id.btn_chatMessageLinksFilterInternal, Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_INTERNAL)),
+ new ListItem(ListItem.TYPE_CHECKBOX_OPTION, R.id.btn_chatMessageLinksFilterExternal, 0, R.string.ChatMessagesFilterExternal, R.id.btn_chatMessageLinksFilterExternal, Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_EXTERNAL))
+ }, (id, result) -> {
+ Settings.instance().setChatLinksFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_INTERNAL,
+ result.get(R.id.btn_chatMessageLinksFilterInternal) == R.id.btn_chatMessageLinksFilterInternal);
+ Settings.instance().setChatLinksFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_EXTERNAL,
+ result.get(R.id.btn_chatMessageLinksFilterExternal) == R.id.btn_chatMessageLinksFilterExternal);
+ tdlib.messagesFilterProvider().updateChatCustomFilterSettings(chatId);
+ if (after != null) {
+ after.run();
+ }
+ });
+ }
}
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java
index f34970c012..e3523dbd18 100644
--- a/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java
+++ b/app/src/main/java/org/thunderdog/challegram/ui/MessagesController.java
@@ -5457,6 +5457,9 @@ private OptionDelegate newMessageOptionDelegate (final TGMessage selectedMessage
tdlib.ui().saveGifs(((List) selectedMessageTag));
}
return true;
+ } else if (id == R.id.btn_messageChangeMessageFilterVisibility) {
+ selectedMessage.setIsHiddenByMessagesFilter(!selectedMessage.isHiddenByMessagesFilter(), true);
+ return true;
} else if (id == R.id.btn_saveFile) {
if (selectedMessageTag != null) {
if (!selectedMessage.canBeSaved()) {
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java b/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java
index df4671c7c1..2daeccbaa8 100644
--- a/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java
+++ b/app/src/main/java/org/thunderdog/challegram/ui/ProfileController.java
@@ -662,6 +662,11 @@ private void showCommonMore () {
strings.append(R.string.Stats);
}
+ if (mode == MODE_CHANNEL && Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_ENABLED)) {
+ ids.append(R.id.more_btn_messagesFilter);
+ strings.append(R.string.MessagesFilter);
+ }
+
tdlib.ui().addDeleteChatOptions(getChatId(), ids, strings, false, true);
if (ids.size() > 0) {
@@ -755,6 +760,9 @@ public void onMoreItemPressed (int id) {
} else if (id == R.id.more_btn_join) {
joinChannel();
return;
+ } else if (id == R.id.more_btn_messagesFilter) {
+ tdlib.ui().openMessageFilterSettings(this, getChatId(), null);
+ return;
}
break;
}
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java
index 5871cabc2b..41a1a50963 100644
--- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java
+++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsBlockedController.java
@@ -217,7 +217,7 @@ protected void setUser (ListItem item, int position, UserView userView, boolean
}
};
buildCells();
- ViewSupport.setThemedBackground(recyclerView, ColorId.filling, this);
+ // ViewSupport.setThemedBackground(recyclerView, ColorId.filling, this);
RemoveHelper.attach(recyclerView, new RemoveHelper.Callback() {
@Override
public boolean canRemove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) {
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterChannelsController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterChannelsController.java
new file mode 100644
index 0000000000..2beea960f0
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterChannelsController.java
@@ -0,0 +1,147 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.ui;
+
+import android.content.Context;
+import android.view.View;
+
+import org.drinkless.tdlib.TdApi;
+import org.thunderdog.challegram.R;
+import org.thunderdog.challegram.core.Lang;
+import org.thunderdog.challegram.data.TGFoundChat;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.tool.UI;
+import org.thunderdog.challegram.unsorted.Settings;
+import org.thunderdog.challegram.v.CustomRecyclerView;
+import org.thunderdog.challegram.widget.BetterChatView;
+
+import java.util.ArrayList;
+
+import me.vkryl.td.ChatId;
+
+public class SettingsMessagesFilterChannelsController extends RecyclerViewController implements View.OnClickListener {
+
+ private SettingsAdapter adapter;
+
+ public SettingsMessagesFilterChannelsController(Context context, Tdlib tdlib) {
+ super(context, tdlib);
+ }
+
+ @Override
+ protected void onCreateView (Context context, CustomRecyclerView recyclerView) {
+ adapter = new SettingsAdapter(this) {
+ @Override
+ protected void setChatData (ListItem item, int position, BetterChatView chatView) {
+ final TGFoundChat tgFoundChat = (TGFoundChat) item.getData();
+ final long chatId = item.getLongId();
+ final String subtitle = makeSubtitle(chatId);
+
+ if (tgFoundChat != null) {
+ tgFoundChat.setForcedSubtitle(subtitle);
+ chatView.setChat(tgFoundChat);
+ } else {
+ chatView.setTitle(Lang.getString(R.string.MessagesFilterUnknownChat));
+ chatView.setChat(null);
+ }
+ chatView.setSubtitle(subtitle);
+ chatView.invalidate();
+ }
+ };
+
+ buildCells();
+ recyclerView.setAdapter(adapter);
+ }
+
+
+ private void buildCells () {
+ ArrayList items = new ArrayList<>();
+
+ final long[] chatIds = Settings.instance().getFilteredChatIds();
+ boolean isFirst = true;
+ for (long chatId : chatIds) {
+ if (!isFirst) {
+ items.add(new ListItem(ListItem.TYPE_SEPARATOR));
+ }
+ isFirst = false;
+
+ TdApi.Chat chat = tdlib.chat(chatId);
+ if (chat == null) {
+ tdlib.chat(chatId, this::onChatLoaded);
+ }
+ TGFoundChat tgFoundChat = chat != null ? new TGFoundChat(tdlib, null, chatId, false) : null;
+ items.add(new ListItem(ListItem.TYPE_CHAT_BETTER, R.id.channel_filtered).setData(tgFoundChat).setLongId(chatId));
+ }
+
+ adapter.setItems(items, true);
+ }
+
+ private void onChatLoaded (TdApi.Chat chat) {
+ UI.post(() -> {
+ if (!isDestroyed() && chat != null) {
+ final ListItem item = adapter.getItem(adapter.indexOfViewByLongId(chat.id));
+ if (item != null) {
+ item.setData( new TGFoundChat(tdlib, null, chat.id, false));
+ adapter.updateValuedSettingByLongId(chat.id);
+ }
+ }
+ });
+ }
+
+
+
+ @Override
+ public void onClick (View v) {
+ final ListItem item = (ListItem) v.getTag();
+ if (item == null) {
+ return;
+ }
+
+ final long chatId = item.getLongId();
+ tdlib.ui().openMessageFilterSettings(this, chatId, () -> adapter.updateValuedSettingByLongId(chatId));
+ }
+
+
+ /* * */
+
+ @Override
+ public int getId () {
+ return R.id.controller_messagesFilterChannelSettings;
+ }
+
+ @Override
+ public CharSequence getName () {
+ return Lang.getString(R.string.MessagesFilterFilteredChannels);
+ }
+
+
+ /* * */
+
+ private static String makeSubtitle (long chatId) {
+ StringBuilder sb = new StringBuilder();
+ if (Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_INTERNAL)) {
+ sb.append(Lang.getString(R.string.ChatMessagesFilterInternal));
+ }
+ if (Settings.instance().isChatFilterEnabled(chatId, Settings.FILTER_TYPE_LINKS_EXTERNAL)) {
+ if (sb.length() > 0) {
+ sb.append(Lang.getConcatSeparator());
+ }
+ sb.append(Lang.getString(R.string.ChatMessagesFilterExternal));
+ }
+ if (sb.length() == 0) {
+ return Lang.getString(R.string.ChatMessagesFilterNone);
+ }
+ return sb.toString();
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterController.java
new file mode 100644
index 0000000000..60e3c73467
--- /dev/null
+++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsMessagesFilterController.java
@@ -0,0 +1,200 @@
+/*
+ * This file is a part of Telegram X
+ * Copyright © 2014 (tgx-android@pm.me)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * File created on 16/11/2023
+ */
+package org.thunderdog.challegram.ui;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import org.drinkless.tdlib.TdApi;
+import org.thunderdog.challegram.R;
+import org.thunderdog.challegram.component.base.SettingView;
+import org.thunderdog.challegram.core.Lang;
+import org.thunderdog.challegram.telegram.ChatListener;
+import org.thunderdog.challegram.telegram.Tdlib;
+import org.thunderdog.challegram.tool.UI;
+import org.thunderdog.challegram.unsorted.Settings;
+import org.thunderdog.challegram.v.CustomRecyclerView;
+
+import java.util.ArrayList;
+
+public class SettingsMessagesFilterController extends RecyclerViewController implements View.OnClickListener, ChatListener {
+
+ private SettingsAdapter adapter;
+
+ public SettingsMessagesFilterController (Context context, Tdlib tdlib) {
+ super(context, tdlib);
+ }
+
+ @Override
+ protected void onCreateView (Context context, CustomRecyclerView recyclerView) {
+ adapter = new SettingsAdapter(this) {
+ @Override
+ public void setValuedSetting (ListItem item, SettingView view, boolean isUpdate) {
+ final int itemId = item.getId();
+ if (itemId == R.id.btn_messageFilterFilteredChannelsManage) {
+ view.setData(Lang.pluralBold(R.string.xChannels, 42));
+ } else if (itemId == R.id.btn_blockedSenders) {
+ view.setData(getBlockedSendersCount());
+ } else if (itemId == R.id.btn_messageFilterEnabled) {
+ view.getToggler().setRadioEnabled(Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_ENABLED), isUpdate);
+ } else if (itemId == R.id.btn_messageFilterHideBlockedSenders) {
+ view.getToggler().setRadioEnabled(Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS), isUpdate);
+ } else if (itemId == R.id.btn_messageFilterHideBlockedSendersMentions) {
+ view.getToggler().setRadioEnabled(Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS_MENTIONS), isUpdate);
+ }
+ }
+ };
+
+ buildCells();
+ recyclerView.setAdapter(adapter);
+
+ tdlib.send(new TdApi.GetBlockedMessageSenders(new TdApi.BlockListMain(), 0, 1), this::onBlockedSendersResult);
+ tdlib.listeners().subscribeForAnyUpdates(this);
+ }
+
+ private int defaultCellsCount = 0;
+ private boolean settingCellsVisible;
+ private int settingCellsCount = 0;
+
+ private void buildCells () {
+ ArrayList items = new ArrayList<>();
+
+ items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_messageFilterEnabled, 0, R.string.MessagesFilter));
+ items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM));
+ items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.MessagesFilterDesc));
+
+ defaultCellsCount = items.size();
+
+ settingCellsVisible = Settings.instance().getMessagesFilterSetting(Settings.MESSAGES_FILTER_ENABLED);
+ if (settingCellsVisible) {
+ buildSettingsCells(items);
+ }
+
+ adapter.setItems(items, true);
+ }
+
+ private void buildSettingsCells (ArrayList items) {
+ final int size = items.size();
+
+ items.add(new ListItem(ListItem.TYPE_SHADOW_TOP));
+ items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_messageFilterHideBlockedSenders, 0, R.string.MessagesFilterHideBlockedSenders));
+ items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM));
+ items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.MessagesFilterHideBlockedSendersDesc));
+
+ items.add(new ListItem(ListItem.TYPE_SHADOW_TOP));
+ items.add(new ListItem(ListItem.TYPE_RADIO_SETTING, R.id.btn_messageFilterHideBlockedSendersMentions, 0, R.string.MessagesFilterHideBlockedSendersMentions));
+ items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM));
+ items.add(new ListItem(ListItem.TYPE_DESCRIPTION, 0, 0, R.string.MessagesFilterHideBlockedSendersMentionsDesc));
+
+ items.add(new ListItem(ListItem.TYPE_SHADOW_TOP));
+ items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_blockedSenders, 0, R.string.MessagesFilterBlockedSendersManage));
+ /*items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL));
+ items.add(new ListItem(ListItem.TYPE_VALUED_SETTING_COMPACT, R.id.btn_messageFilterFilteredChannelsManage, 0, R.string.MessagesFilterChannelsManage));*/
+ items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM));
+
+ settingCellsCount = items.size() - size;
+ }
+
+ private void setSettingsCellsVisible (boolean visible) {
+ if (visible == settingCellsVisible) {
+ return;
+ }
+ settingCellsVisible = visible;
+ if (visible) {
+ ArrayList items = new ArrayList<>();
+ buildSettingsCells(items);
+ adapter.addItems(defaultCellsCount, items.toArray(new ListItem[0]));
+ } else {
+ adapter.removeRange(defaultCellsCount, settingCellsCount);
+ }
+ }
+
+ @Override
+ public void onClick (View v) {
+ final int id = v.getId();
+ if (id == R.id.btn_messageFilterFilteredChannelsManage) {
+ SettingsMessagesFilterChannelsController c = new SettingsMessagesFilterChannelsController(context, tdlib);
+ navigateTo(c);
+ } else if (id == R.id.btn_blockedSenders) {
+ SettingsBlockedController c = new SettingsBlockedController(context, tdlib);
+ c.setArguments(new TdApi.BlockListMain());
+ navigateTo(c);
+ } else if (id == R.id.btn_messageFilterEnabled) {
+ boolean newValue = adapter.toggleView(v);
+ Settings.instance().setMessagesFilterSetting(Settings.MESSAGES_FILTER_ENABLED, newValue);
+ setSettingsCellsVisible(newValue);
+ } else if (id == R.id.btn_messageFilterHideBlockedSenders) {
+ Settings.instance().setMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS, adapter.toggleView(v));
+ } else if (id == R.id.btn_messageFilterHideBlockedSendersMentions) {
+ Settings.instance().setMessagesFilterSetting(Settings.MESSAGES_FILTER_HIDE_BLOCKED_SENDERS_MENTIONS, adapter.toggleView(v));
+ }
+ }
+
+
+ /* Blocked users */
+
+ private int blockedSendersCount = -1;
+
+ private CharSequence getBlockedSendersCount () {
+ return blockedSendersCount == -1 ? Lang.getString(R.string.LoadingInformation) : blockedSendersCount > 0 ? Lang.pluralBold(R.string.xSenders, blockedSendersCount) : Lang.getString(R.string.BlockedNone);
+ }
+
+ private void onBlockedSendersResult (TdApi.MessageSenders senders, TdApi.Error error) {
+ UI.post(() -> {
+ if (isDestroyed()) {
+ return;
+ }
+
+ final int totalCount = senders.totalCount;
+ if (this.blockedSendersCount != totalCount) {
+ this.blockedSendersCount = totalCount;
+ adapter.updateValuedSettingById(R.id.btn_blockedSenders);
+ }
+ });
+ }
+
+
+ /* * */
+
+ @Override
+ public int getId () {
+ return R.id.controller_messagesFilterSettings;
+ }
+
+ @Override
+ public CharSequence getName () {
+ return Lang.getString(R.string.MessagesFilter);
+ }
+
+ @Override
+ public void destroy () {
+ super.destroy();
+ tdlib.listeners().unsubscribeFromAnyUpdates(this);
+ }
+
+
+ /* Tdlib listeners */
+
+ @Override
+ public void onChatBlockListChanged (long chatId, @Nullable TdApi.BlockList blockList) {
+ runOnUiThread(() -> {
+ if (!isDestroyed()) {
+ tdlib.send(new TdApi.GetBlockedMessageSenders(new TdApi.BlockListMain(), 0, 1), this::onBlockedSendersResult);
+ }
+ }, 350L);
+ }
+}
diff --git a/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java b/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java
index a5f85eac62..abd2f77087 100644
--- a/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java
+++ b/app/src/main/java/org/thunderdog/challegram/ui/SettingsThemeController.java
@@ -401,6 +401,8 @@ protected void setValuedSetting (ListItem item, SettingView v, boolean isUpdate)
items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_chatBackground, 0, R.string.Wallpaper));
items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL));
items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_chatFontSize, 0, R.string.TextSize));
+ items.add(new ListItem(ListItem.TYPE_SEPARATOR_FULL));
+ items.add(new ListItem(ListItem.TYPE_SETTING, R.id.btn_messageFilterSettings, 0, R.string.MessagesFilter));
items.add(new ListItem(ListItem.TYPE_SHADOW_BOTTOM));
items.add(new ListItem(ListItem.TYPE_HEADER, 0, 0, R.string.ColorTheme));
@@ -1251,6 +1253,9 @@ public void onClick (View v) {
MessagesController controller = new MessagesController(context, tdlib);
controller.setArguments(new MessagesController.Arguments(MessagesController.PREVIEW_MODE_FONT_SIZE, null, null));
navigateTo(controller);
+ } else if (viewId == R.id.btn_messageFilterSettings) {
+ SettingsMessagesFilterController controller = new SettingsMessagesFilterController(context, tdlib);
+ navigateTo(controller);
} else if (viewId == R.id.btn_chatBackground) {
if (!context().permissions().requestReadExternalStorage(Permissions.ReadType.IMAGES, grantType ->
openWallpaperSetup()
diff --git a/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java b/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java
index 3292d48320..f152f43a4e 100644
--- a/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java
+++ b/app/src/main/java/org/thunderdog/challegram/unsorted/Settings.java
@@ -31,6 +31,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
+import com.google.common.primitives.Longs;
import com.otaliastudios.transcoder.strategy.DefaultVideoStrategy;
import org.drinkless.tdlib.Client;
@@ -96,6 +97,7 @@
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -240,6 +242,7 @@ public static Settings instance () {
private static final String KEY_CAMERA_VOLUME_CONTROL = "settings_camera_control";
private static final String KEY_CHAT_FOLDER_STYLE = "settings_folders_style";
private static final String KEY_CHAT_FOLDER_OPTIONS = "settings_folders_options";
+ private static final String KEY_MESSAGES_FILTER_FLAGS = "settings_messages_filter";
private static final String KEY_TDLIB_VERBOSITY = "settings_tdlib_verbosity";
private static final String KEY_TDLIB_DEBUG_PREFIX = "settings_tdlib_allow_debug";
@@ -435,7 +438,7 @@ private static String key (String key, int accountId) {
@Nullable
private Integer _settings;
@Nullable
- private Long _newSettings, _experiments;
+ private Long _newSettings, _experiments, _messagesFilter;
public static final int NIGHT_MODE_NONE = 0;
public static final int NIGHT_MODE_AUTO = 1;
@@ -1300,6 +1303,28 @@ public void setChatFolderStyle (@ChatFolderStyle int style) {
return _chatFolderStyle;
}
+ public static final long MESSAGES_FILTER_ENABLED = 1;
+ public static final long MESSAGES_FILTER_HIDE_BLOCKED_SENDERS = 1 << 1;
+ public static final long MESSAGES_FILTER_HIDE_BLOCKED_SENDERS_MENTIONS = 1 << 2;
+
+ private long getMessagesFilterSettings () {
+ if (_messagesFilter == null)
+ _messagesFilter = pmc.getLong(KEY_MESSAGES_FILTER_FLAGS, 0);
+ return _messagesFilter;
+ }
+
+ public boolean getMessagesFilterSetting (long flag) {
+ final boolean filterEnabled = flag != MESSAGES_FILTER_ENABLED ?
+ getMessagesFilterSetting(MESSAGES_FILTER_ENABLED) : true;
+ return filterEnabled && BitwiseUtils.hasFlag(getMessagesFilterSettings(), flag);
+ }
+
+ public void setMessagesFilterSetting (long flag, boolean enabled) {
+ _messagesFilter = BitwiseUtils.setFlag(getMessagesFilterSettings(), flag, enabled);
+ pmc.putLong(KEY_MESSAGES_FILTER_FLAGS, _messagesFilter);
+ }
+
+
private long makeDefaultNewSettings () {
long settings = 0;
@@ -6960,4 +6985,85 @@ public boolean chatFoldersEnabled () {
public boolean showPeerIds () {
return isExperimentEnabled(EXPERIMENT_FLAG_SHOW_PEER_IDS);
}
+
+
+
+
+ /* Messages Filter */
+
+ private static final String KEY_FILTERED_CHATS = "filtered_chat_ids";
+ private static final String KEY_CHAT_FILTER_FLAGS = "chat_filter_flags_";
+
+ public static final int FILTER_TYPE_LINKS_INTERNAL = 1;
+ public static final int FILTER_TYPE_LINKS_EXTERNAL = 1 << 1;
+
+ private final HashMap filterFlagsCache = new HashMap<>();
+ private final HashSet filteredChatIds = new HashSet<>();
+ private boolean messagesFilterInited;
+
+ private void initMessagesFilterSettings () {
+ if (messagesFilterInited) {
+ return;
+ }
+
+ final long[] chatIds = pmc.getLongArray(KEY_FILTERED_CHATS);
+ if (chatIds != null) {
+ for (long chatId : chatIds) {
+ filteredChatIds.add(chatId);
+ }
+ }
+ messagesFilterInited = true;
+ }
+
+ private long getChatEnabledFilters (long chatId) {
+ initMessagesFilterSettings();
+
+ Long cached = filterFlagsCache.get(chatId);
+ if (cached != null) {
+ return cached;
+ } else {
+ long flags = pmc.getLong(KEY_CHAT_FILTER_FLAGS + chatId, 0);
+ filterFlagsCache.put(chatId, flags);
+ return flags;
+ }
+ }
+
+ private void setChatEnabledFilters (long chatId, long enabledFilters) {
+ initMessagesFilterSettings();
+
+ final boolean contains = filteredChatIds.contains(chatId);
+ final String key = KEY_CHAT_FILTER_FLAGS + chatId;
+
+ filterFlagsCache.put(chatId, enabledFilters);
+
+ LevelDB editor = pmc.edit();
+ if (enabledFilters != 0) {
+ pmc.putLong(key, enabledFilters);
+ if (!contains) {
+ filteredChatIds.add(chatId);
+ editor.putLongArray(KEY_FILTERED_CHATS, Longs.toArray(filteredChatIds));
+ }
+ } else {
+ pmc.remove(key);
+ if (contains) {
+ filteredChatIds.remove(chatId);
+ editor.putLongArray(KEY_FILTERED_CHATS, Longs.toArray(filteredChatIds));
+ }
+ }
+
+ editor.apply();
+ }
+
+ public long[] getFilteredChatIds () {
+ initMessagesFilterSettings();
+ return Longs.toArray(filteredChatIds);
+ }
+
+ public boolean isChatFilterEnabled (long chatId, int flag) {
+ return BitwiseUtils.hasFlag(getChatEnabledFilters(chatId), flag);
+ }
+
+ public void setChatLinksFilterEnabled (long chatId, int flag, boolean enabled) {
+ setChatEnabledFilters(chatId, BitwiseUtils.setFlag(getChatEnabledFilters(chatId), flag, enabled));
+ }
}
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
index 64adc8e350..1a30ad5f8a 100644
--- a/app/src/main/res/values/ids.xml
+++ b/app/src/main/res/values/ids.xml
@@ -88,6 +88,7 @@
+
@@ -153,6 +154,8 @@
+
+
@@ -273,6 +276,7 @@
+
@@ -523,6 +527,11 @@
+
+
+
+
+
@@ -1098,9 +1107,15 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1c3c81d13d..bafe1f82ea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4911,4 +4911,27 @@
This message is from another chat. Tap again to view.
Swipe to choose specific link preview
+ Messages Filter
+ When enabled, content of messages that match at least one of the criteria below, will be hidden and minimized.
+
+ Groups and Chats
+ Blocked senders in groups
+ Hide message content in group chats, if it comes from a blocked sender.
+ Mentions of blocked chats
+ Hide message content, if it includes a reply, mention or direct link to the blocked chat.
+
+ Channels
+ Manage Filtered Channels
+ Filtered Channels
+ Manage Blocked Senders
+
+ Show Message
+
+ Hides all messages with links to other chats, channels or external sites.
+ Telegram links
+ External links
+ None
+
+ Unknown Chat
+