That fuck shit the fascists are using
at master 306 lines 12 kB view raw
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}