That fuck shit the fascists are using
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}