That fuck shit the fascists are using
at master 133 lines 5.0 kB view raw
1/* 2 * Copyright 2023 Signal Messenger, LLC 3 * SPDX-License-Identifier: AGPL-3.0-only 4 */ 5 6package org.tm.archive.conversation; 7 8import android.content.Context; 9 10import androidx.annotation.NonNull; 11import androidx.annotation.Nullable; 12import androidx.lifecycle.Lifecycle; 13import androidx.lifecycle.LifecycleOwner; 14import androidx.recyclerview.widget.LinearLayoutManager; 15 16import com.annimon.stream.Stream; 17 18import org.signal.core.util.concurrent.SignalExecutors; 19import org.signal.core.util.logging.Log; 20import org.tm.archive.database.MessageTable; 21import org.tm.archive.database.SignalDatabase; 22import org.tm.archive.database.ThreadTable; 23import org.tm.archive.database.model.MessageRecord; 24import org.tm.archive.database.model.ReactionRecord; 25import org.tm.archive.dependencies.ApplicationDependencies; 26import org.tm.archive.notifications.MarkReadReceiver; 27import org.tm.archive.notifications.v2.ConversationId; 28import org.tm.archive.util.Debouncer; 29import org.tm.archive.util.concurrent.SerialMonoLifoExecutor; 30 31import java.util.List; 32import java.util.Optional; 33import java.util.concurrent.Executor; 34 35public class MarkReadHelper { 36 private static final String TAG = Log.tag(MarkReadHelper.class); 37 38 private static final long DEBOUNCE_TIMEOUT = 100; 39 private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED); 40 41 private final ConversationId conversationId; 42 private final Context context; 43 private final LifecycleOwner lifecycleOwner; 44 private final Debouncer debouncer = new Debouncer(DEBOUNCE_TIMEOUT); 45 private long latestTimestamp; 46 private boolean ignoreViewReveals = false; 47 48 public MarkReadHelper(@NonNull ConversationId conversationId, @NonNull Context context, @NonNull LifecycleOwner lifecycleOwner) { 49 this.conversationId = conversationId; 50 this.context = context.getApplicationContext(); 51 this.lifecycleOwner = lifecycleOwner; 52 } 53 54 public void onViewsRevealed(long timestamp) { 55 if (timestamp <= latestTimestamp || lifecycleOwner.getLifecycle().getCurrentState() != Lifecycle.State.RESUMED || ignoreViewReveals) { 56 return; 57 } 58 59 latestTimestamp = timestamp; 60 61 debouncer.publish(() -> { 62 EXECUTOR.execute(() -> { 63 ThreadTable threadTable = SignalDatabase.threads(); 64 List<MessageTable.MarkedMessageInfo> infos = threadTable.setReadSince(conversationId, false, timestamp); 65 66 Log.d(TAG, "Marking " + infos.size() + " messages as read."); 67 68 ApplicationDependencies.getMessageNotifier().updateNotification(context); 69 MarkReadReceiver.process(infos); 70 }); 71 }); 72 } 73 74 /** 75 * Prevent calls to {@link #onViewsRevealed(long)} from causing messages to be marked read. 76 * <p> 77 * This is particularly useful when the conversation could move around after views are 78 * displayed (e.g., initial scrolling to start position). 79 */ 80 public void ignoreViewReveals() { 81 ignoreViewReveals = true; 82 } 83 84 /** 85 * Stop preventing calls to {@link #onViewsRevealed(long)} from not marking messages as read. 86 * 87 * @param timestamp Timestamp of most recent reveal messages, same as usually provided to {@code onViewsRevealed} 88 */ 89 public void stopIgnoringViewReveals(@Nullable Long timestamp) { 90 this.ignoreViewReveals = false; 91 if (timestamp != null) { 92 onViewsRevealed(timestamp); 93 } 94 } 95 96 /** 97 * Given the adapter and manager, figure out the timestamp to mark read up to. 98 * 99 * @param conversationAdapter The conversation thread's adapter 100 * @param layoutManager The conversation thread's layout manager 101 * @return A Present(Long) if there's a timestamp to proceed with, or Empty if this request should be ignored. 102 */ 103 @SuppressWarnings("resource") 104 public static @NonNull Optional<Long> getLatestTimestamp(@NonNull ConversationAdapterBridge conversationAdapter, 105 @NonNull LinearLayoutManager layoutManager) 106 { 107 if (conversationAdapter.hasNoConversationMessages()) { 108 return Optional.empty(); 109 } 110 111 int position = layoutManager.findFirstVisibleItemPosition(); 112 if (position == -1 || position == layoutManager.getItemCount() - 1) { 113 return Optional.empty(); 114 } 115 116 ConversationMessage item = conversationAdapter.getConversationMessage(position); 117 if (item == null) { 118 item = conversationAdapter.getConversationMessage(position + 1); 119 } 120 121 if (item != null) { 122 MessageRecord record = item.getMessageRecord(); 123 long latestReactionReceived = Stream.of(record.getReactions()) 124 .map(ReactionRecord::getDateReceived) 125 .max(Long::compareTo) 126 .orElse(0L); 127 128 return Optional.of(Math.max(record.getDateReceived(), latestReactionReceived)); 129 } 130 131 return Optional.empty(); 132 } 133}