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 +