That fuck shit the fascists are using
1package org.tm.archive.messagedetails;
2
3import android.content.ClipData;
4import android.content.ClipboardManager;
5import android.content.Context;
6import android.os.CountDownTimer;
7import android.text.SpannableStringBuilder;
8import android.text.Spanned;
9import android.text.style.StyleSpan;
10import android.view.View;
11import android.view.ViewGroup;
12import android.view.ViewStub;
13import android.widget.TextView;
14
15import androidx.annotation.NonNull;
16import androidx.annotation.Nullable;
17import androidx.lifecycle.DefaultLifecycleObserver;
18import androidx.lifecycle.LifecycleOwner;
19import androidx.recyclerview.widget.RecyclerView;
20
21import androidx.media3.common.MediaItem;
22
23import com.bumptech.glide.RequestManager;
24
25import org.signal.core.util.concurrent.SignalExecutors;
26import org.tm.archive.R;
27import org.tm.archive.conversation.ConversationItem;
28import org.tm.archive.conversation.ConversationMessage;
29import org.tm.archive.conversation.colors.Colorizable;
30import org.tm.archive.conversation.colors.Colorizer;
31import org.tm.archive.conversation.ConversationItemDisplayMode;
32import org.tm.archive.database.model.MessageRecord;
33import org.tm.archive.giph.mp4.GiphyMp4Playable;
34import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
35import org.tm.archive.messagedetails.MessageDetailsAdapter.Callbacks;
36import org.tm.archive.sms.MessageSender;
37import org.tm.archive.util.DateUtils;
38import org.tm.archive.util.ExpirationUtil;
39import org.tm.archive.util.FeatureFlags;
40import org.tm.archive.util.Projection;
41import org.tm.archive.util.ProjectionList;
42
43import java.sql.Date;
44import java.text.SimpleDateFormat;
45import java.util.HashSet;
46import java.util.Locale;
47import java.util.Optional;
48import java.util.concurrent.TimeUnit;
49
50final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
51 private final TextView sentDate;
52 private final TextView receivedDate;
53 private final TextView expiresIn;
54 private final TextView transport;
55 private final TextView errorText;
56 private final View resendButton;
57 private final View messageMetadata;
58 private final View internalDetailsButton;
59 private final ViewStub updateStub;
60 private final ViewStub sentStub;
61 private final ViewStub receivedStub;
62 private final Colorizer colorizer;
63 private final RequestManager requestManager;
64 private final Callbacks callbacks;
65
66 private ConversationItem conversationItem;
67 private CountDownTimer expiresUpdater;
68
69 MessageHeaderViewHolder(@NonNull View itemView, RequestManager requestManager, @NonNull Colorizer colorizer, @NonNull Callbacks callbacks) {
70 super(itemView);
71 this.requestManager = requestManager;
72 this.colorizer = colorizer;
73 this.callbacks = callbacks;
74
75 sentDate = itemView.findViewById(R.id.message_details_header_sent_time);
76 receivedDate = itemView.findViewById(R.id.message_details_header_received_time);
77 expiresIn = itemView.findViewById(R.id.message_details_header_expires_in);
78 transport = itemView.findViewById(R.id.message_details_header_transport);
79 errorText = itemView.findViewById(R.id.message_details_header_error_text);
80 resendButton = itemView.findViewById(R.id.message_details_header_resend_button);
81 messageMetadata = itemView.findViewById(R.id.message_details_header_message_metadata);
82 internalDetailsButton = itemView.findViewById(R.id.message_details_header_internal_details_button);
83 updateStub = itemView.findViewById(R.id.message_details_header_message_view_update);
84 sentStub = itemView.findViewById(R.id.message_details_header_message_view_sent_multimedia);
85 receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
86 }
87
88 void bind(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage) {
89 MessageRecord messageRecord = conversationMessage.getMessageRecord();
90 bindMessageView(lifecycleOwner, conversationMessage);
91 bindErrorState(messageRecord);
92 bindSentReceivedDates(messageRecord);
93 bindExpirationTime(lifecycleOwner, messageRecord);
94 bindTransport(messageRecord);
95 bindInternalDetails(messageRecord);
96 }
97
98 private void bindInternalDetails(MessageRecord messageRecord) {
99 if (!FeatureFlags.internalUser()) {
100 internalDetailsButton.setVisibility(View.GONE);
101 return;
102 }
103
104 internalDetailsButton.setVisibility(View.VISIBLE);
105 internalDetailsButton.setOnClickListener(v -> callbacks.onInternalDetailsClicked(messageRecord));
106 }
107
108 private void bindMessageView(@NonNull LifecycleOwner lifecycleOwner, @NonNull ConversationMessage conversationMessage) {
109 if (conversationItem == null) {
110 if (conversationMessage.getMessageRecord().isGroupAction()) {
111 conversationItem = (ConversationItem) updateStub.inflate();
112 } else if (conversationMessage.getMessageRecord().isOutgoing()) {
113 conversationItem = (ConversationItem) sentStub.inflate();
114 } else {
115 conversationItem = (ConversationItem) receivedStub.inflate();
116 }
117 }
118 conversationItem.bind(lifecycleOwner,
119 conversationMessage,
120 Optional.empty(),
121 Optional.empty(),
122 requestManager,
123 Locale.getDefault(),
124 new HashSet<>(),
125 conversationMessage.getMessageRecord().getToRecipient(),
126 null,
127 false,
128 false,
129 false,
130 true,
131 colorizer,
132 ConversationItemDisplayMode.Detailed.INSTANCE);
133 }
134
135 private void bindErrorState(MessageRecord messageRecord) {
136 if (messageRecord.hasFailedWithNetworkFailures()) {
137 errorText.setVisibility(View.VISIBLE);
138 resendButton.setVisibility(View.VISIBLE);
139 resendButton.setOnClickListener(unused -> {
140 resendButton.setOnClickListener(null);
141 SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(itemView.getContext().getApplicationContext(), messageRecord));
142 });
143 messageMetadata.setVisibility(View.GONE);
144 } else if (messageRecord.isFailed()) {
145 errorText.setVisibility(View.VISIBLE);
146 resendButton.setVisibility(View.GONE);
147 resendButton.setOnClickListener(null);
148 messageMetadata.setVisibility(View.GONE);
149 } else {
150 errorText.setVisibility(View.GONE);
151 resendButton.setVisibility(View.GONE);
152 resendButton.setOnClickListener(null);
153 messageMetadata.setVisibility(View.VISIBLE);
154 }
155 }
156
157 private void bindSentReceivedDates(MessageRecord messageRecord) {
158 sentDate.setOnLongClickListener(null);
159 receivedDate.setOnLongClickListener(null);
160
161 if (messageRecord.isPending() || messageRecord.isFailed()) {
162 sentDate.setText(formatBoldString(R.string.message_details_header_sent, "-"));
163 receivedDate.setVisibility(View.GONE);
164 } else {
165 Locale dateLocale = Locale.getDefault();
166 SimpleDateFormat dateFormatter = DateUtils.getDetailedDateFormatter(itemView.getContext(), dateLocale);
167 sentDate.setText(formatBoldString(R.string.message_details_header_sent, dateFormatter.format(new Date(messageRecord.getDateSent()))));
168 sentDate.setOnLongClickListener(v -> {
169 copyToClipboard(String.valueOf(messageRecord.getDateSent()));
170 return true;
171 });
172
173 if (messageRecord.getDateReceived() != messageRecord.getDateSent() && !messageRecord.isOutgoing()) {
174 receivedDate.setText(formatBoldString(R.string.message_details_header_received, dateFormatter.format(new Date(messageRecord.getDateReceived()))));
175 receivedDate.setOnLongClickListener(v -> {
176 copyToClipboard(String.valueOf(messageRecord.getDateReceived()));
177 return true;
178 });
179 receivedDate.setVisibility(View.VISIBLE);
180 } else {
181 receivedDate.setVisibility(View.GONE);
182 }
183 }
184 }
185
186 private void bindExpirationTime(@NonNull LifecycleOwner lifecycleOwner, @NonNull MessageRecord messageRecord) {
187 if (expiresUpdater != null) {
188 expiresUpdater.cancel();
189 expiresUpdater = null;
190 }
191
192 if (messageRecord.getExpiresIn() <= 0 || messageRecord.getExpireStarted() <= 0) {
193 expiresIn.setVisibility(View.GONE);
194 return;
195 }
196
197 expiresIn.setVisibility(View.VISIBLE);
198
199 lifecycleOwner.getLifecycle().addObserver(new DefaultLifecycleObserver() {
200 @Override
201 public void onResume(@NonNull LifecycleOwner owner) {
202 if (expiresUpdater != null) {
203 expiresUpdater.cancel();
204 }
205
206 long elapsed = System.currentTimeMillis() - messageRecord.getExpireStarted();
207 long remaining = messageRecord.getExpiresIn() - elapsed;
208 long updateRate = (remaining < TimeUnit.HOURS.toMillis(1)) ? TimeUnit.SECONDS.toMillis(1) : TimeUnit.MINUTES.toMillis(1);
209
210 expiresUpdater = new CountDownTimer(remaining, updateRate) {
211 @Override
212 public void onTick(long millisUntilFinished) {
213 int expirationTime = Math.max((int) (TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)), 1);
214 String duration = ExpirationUtil.getExpirationDisplayValue(itemView.getContext(), expirationTime);
215
216 expiresIn.setText(formatBoldString(R.string.message_details_header_disappears, duration));
217 }
218
219 @Override
220 public void onFinish() {}
221 };
222 expiresUpdater.start();
223 }
224
225 @Override
226 public void onPause(@NonNull LifecycleOwner owner) {
227 if (expiresUpdater != null) {
228 expiresUpdater.cancel();
229 expiresUpdater = null;
230 }
231 }
232 });
233 }
234
235 private void bindTransport(MessageRecord messageRecord) {
236 final String transportText;
237 if (messageRecord.isOutgoing() && messageRecord.isFailed()) {
238 transportText = "-";
239 } else if (messageRecord.isPending()) {
240 transportText = itemView.getContext().getString(R.string.ConversationFragment_pending);
241 } else if (messageRecord.isPush()) {
242 transportText = itemView.getContext().getString(R.string.ConversationFragment_push);
243 } else if (messageRecord.isMms()) {
244 transportText = itemView.getContext().getString(R.string.ConversationFragment_mms);
245 } else {
246 transportText = itemView.getContext().getString(R.string.ConversationFragment_sms);
247 }
248
249 transport.setText(formatBoldString(R.string.message_details_header_via, transportText));
250 }
251
252 private CharSequence formatBoldString(int boldTextRes, CharSequence otherText) {
253 SpannableStringBuilder builder = new SpannableStringBuilder();
254 StyleSpan boldSpan = new StyleSpan(android.graphics.Typeface.BOLD);
255 CharSequence boldText = itemView.getContext().getString(boldTextRes);
256
257 builder.append(boldText).append(" ").append(otherText);
258 builder.setSpan(boldSpan, 0, boldText.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
259
260 return builder;
261 }
262
263 private void copyToClipboard(String text) {
264 ((ClipboardManager) itemView.getContext().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("text", text));
265 }
266
267 @Override
268 public void showProjectionArea() {
269 conversationItem.showProjectionArea();
270 }
271
272 @Override
273 public void hideProjectionArea() {
274 conversationItem.hideProjectionArea();
275 }
276
277 @Override
278 public @Nullable MediaItem getMediaItem() {
279 return conversationItem.getMediaItem();
280 }
281
282 @Override
283 public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() {
284 return conversationItem.getPlaybackPolicyEnforcer();
285 }
286
287 @Override
288 public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerview) {
289 return conversationItem.getGiphyMp4PlayableProjection(recyclerview);
290 }
291
292 @Override
293 public boolean canPlayContent() {
294 return conversationItem.canPlayContent();
295 }
296
297 @Override
298 public boolean shouldProjectContent() {
299 return conversationItem.shouldProjectContent();
300 }
301
302 @Override
303 public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) {
304 return conversationItem.getColorizerProjections(coordinateRoot);
305 }
306}