That fuck shit the fascists are using
at master 831 lines 30 kB view raw
1/* 2 * Copyright (C) 2011 Whisper Systems 3 * 4 * This program is free software: you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation, either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17package org.tm.archive.conversation; 18 19import android.annotation.SuppressLint; 20import android.content.Context; 21import android.os.Build; 22import android.view.LayoutInflater; 23import android.view.View; 24import android.view.ViewGroup; 25import android.widget.FrameLayout; 26import android.widget.TextView; 27 28import androidx.annotation.ColorInt; 29import androidx.annotation.DrawableRes; 30import androidx.annotation.LayoutRes; 31import androidx.annotation.MainThread; 32import androidx.annotation.NonNull; 33import androidx.annotation.Nullable; 34import androidx.core.content.ContextCompat; 35import androidx.lifecycle.LifecycleOwner; 36import androidx.recyclerview.widget.DiffUtil; 37import androidx.recyclerview.widget.ListAdapter; 38import androidx.recyclerview.widget.RecyclerView; 39 40import androidx.media3.common.MediaItem; 41 42import com.bumptech.glide.RequestManager; 43 44import org.signal.core.util.logging.Log; 45import org.signal.paging.PagingController; 46import org.tm.archive.BindableConversationItem; 47import org.tm.archive.R; 48import org.tm.archive.conversation.colors.Colorizable; 49import org.tm.archive.conversation.colors.Colorizer; 50import org.tm.archive.conversation.mutiselect.MultiselectPart; 51import org.tm.archive.database.model.MmsMessageRecord; 52import org.tm.archive.database.model.MessageRecord; 53import org.tm.archive.giph.mp4.GiphyMp4Playable; 54import org.tm.archive.giph.mp4.GiphyMp4PlaybackPolicyEnforcer; 55import org.tm.archive.recipients.RecipientId; 56import org.tm.archive.util.CachedInflater; 57import org.tm.archive.util.DateUtils; 58import org.tm.archive.util.Projection; 59import org.tm.archive.util.ProjectionList; 60import org.tm.archive.util.StickyHeaderDecoration; 61import org.tm.archive.util.ThemeUtil; 62import org.tm.archive.util.ViewUtil; 63 64import java.util.Calendar; 65import java.util.HashSet; 66import java.util.List; 67import java.util.Locale; 68import java.util.Objects; 69import java.util.Optional; 70import java.util.Set; 71 72/** 73 * Adapter that renders a conversation. 74 * 75 * Important spacial thing to keep in mind: The adapter is intended to be shown on a reversed layout 76 * manager, so position 0 is at the bottom of the screen. That's why the "header" is at the bottom, 77 * the "footer" is at the top, and we refer to the "next" record as having a lower index. 78 */ 79public class ConversationAdapter 80 extends ListAdapter<ConversationMessage, RecyclerView.ViewHolder> 81 implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>, 82 ConversationAdapterBridge 83{ 84 85 private static final String TAG = Log.tag(ConversationAdapter.class); 86 87 public static final int HEADER_TYPE_POPOVER_DATE = 1; 88 public static final int HEADER_TYPE_INLINE_DATE = 2; 89 public static final int HEADER_TYPE_LAST_SEEN = 3; 90 91 private static final int MESSAGE_TYPE_OUTGOING_MULTIMEDIA = 0; 92 private static final int MESSAGE_TYPE_OUTGOING_TEXT = 1; 93 private static final int MESSAGE_TYPE_INCOMING_MULTIMEDIA = 2; 94 private static final int MESSAGE_TYPE_INCOMING_TEXT = 3; 95 private static final int MESSAGE_TYPE_UPDATE = 4; 96 private static final int MESSAGE_TYPE_HEADER = 5; 97 public static final int MESSAGE_TYPE_FOOTER = 6; 98 private static final int MESSAGE_TYPE_PLACEHOLDER = 7; 99 100 private final ItemClickListener clickListener; 101 private final Context context; 102 private final LifecycleOwner lifecycleOwner; 103 private final RequestManager requestManager; 104 private final Locale locale; 105 private final Set<MultiselectPart> selected; 106 private final Calendar calendar; 107 108 private String searchQuery; 109 private ConversationMessage recordToPulse; 110 private View typingView; 111 private View footerView; 112 private PagingController pagingController; 113 private boolean hasWallpaper; 114 private boolean isMessageRequestAccepted; 115 private ConversationMessage inlineContent; 116 private Colorizer colorizer; 117 private boolean isTypingViewEnabled; 118 private ConversationItemDisplayMode displayMode; 119 private PulseRequest pulseRequest; 120 121 public ConversationAdapter(@NonNull Context context, 122 @NonNull LifecycleOwner lifecycleOwner, 123 @NonNull RequestManager requestManager, 124 @NonNull Locale locale, 125 @Nullable ItemClickListener clickListener, 126 boolean hasWallpaper, 127 @NonNull Colorizer colorizer) 128 { 129 super(new DiffUtil.ItemCallback<ConversationMessage>() { 130 @Override 131 public boolean areItemsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) { 132 return oldItem.getMessageRecord().getId() == newItem.getMessageRecord().getId(); 133 } 134 135 @Override 136 public boolean areContentsTheSame(@NonNull ConversationMessage oldItem, @NonNull ConversationMessage newItem) { 137 return false; 138 } 139 }); 140 141 this.lifecycleOwner = lifecycleOwner; 142 this.context = context; 143 144 this.requestManager = requestManager; 145 this.locale = locale; 146 this.clickListener = clickListener; 147 this.selected = new HashSet<>(); 148 this.calendar = Calendar.getInstance(); 149 this.hasWallpaper = hasWallpaper; 150 this.isMessageRequestAccepted = true; 151 this.colorizer = colorizer; 152 } 153 154 @Override 155 public int getItemViewType(int position) { 156 if (isTypingViewEnabled() && position == 0) { 157 return MESSAGE_TYPE_HEADER; 158 } 159 160 if (hasFooter() && position == getItemCount() - 1) { 161 return MESSAGE_TYPE_FOOTER; 162 } 163 164 ConversationMessage conversationMessage = getItem(position); 165 MessageRecord messageRecord = (conversationMessage != null) ? conversationMessage.getMessageRecord() : null; 166 167 if (messageRecord == null) { 168 return MESSAGE_TYPE_PLACEHOLDER; 169 } else if (messageRecord.isUpdate()) { 170 return MESSAGE_TYPE_UPDATE; 171 } else if (messageRecord.isOutgoing()) { 172 return conversationMessage.isTextOnly(context) ? MESSAGE_TYPE_OUTGOING_TEXT : MESSAGE_TYPE_OUTGOING_MULTIMEDIA; 173 } else { 174 return conversationMessage.isTextOnly(context) ? MESSAGE_TYPE_INCOMING_TEXT : MESSAGE_TYPE_INCOMING_MULTIMEDIA; 175 } 176 } 177 178 @SuppressLint("ClickableViewAccessibility") 179 @Override 180 public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 181 switch (viewType) { 182 case MESSAGE_TYPE_INCOMING_TEXT: 183 case MESSAGE_TYPE_INCOMING_MULTIMEDIA: 184 case MESSAGE_TYPE_OUTGOING_TEXT: 185 case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: 186 case MESSAGE_TYPE_UPDATE: 187 View itemView = CachedInflater.from(parent.getContext()).inflate(getLayoutForViewType(viewType), parent, false); 188 BindableConversationItem bindable = (BindableConversationItem) itemView; 189 190 itemView.setOnClickListener((v) -> { 191 if (clickListener != null) { 192 clickListener.onItemClick(bindable.getMultiselectPartForLatestTouch()); 193 } 194 }); 195 196 itemView.setOnLongClickListener((v) -> { 197 if (clickListener != null) { 198 clickListener.onItemLongClick(itemView, bindable.getMultiselectPartForLatestTouch()); 199 } 200 201 return true; 202 }); 203 204 bindable.setEventListener(clickListener); 205 206 return new ConversationViewHolder(itemView); 207 case MESSAGE_TYPE_PLACEHOLDER: 208 View v = new FrameLayout(parent.getContext()); 209 v.setLayoutParams(new FrameLayout.LayoutParams(1, ViewUtil.dpToPx(100))); 210 return new PlaceholderViewHolder(v); 211 case MESSAGE_TYPE_HEADER: 212 return new HeaderViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false)); 213 case MESSAGE_TYPE_FOOTER: 214 return new FooterViewHolder(CachedInflater.from(parent.getContext()).inflate(R.layout.cursor_adapter_header_footer_view, parent, false)); 215 default: 216 throw new IllegalStateException("Cannot create viewholder for type: " + viewType); 217 } 218 } 219 220 private boolean containsValidPayload(@NonNull List<Object> payloads) { 221 return payloads.contains(PAYLOAD_TIMESTAMP) || payloads.contains(PAYLOAD_NAME_COLORS) || payloads.contains(PAYLOAD_SELECTED); 222 } 223 224 @Override 225 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) { 226 if (containsValidPayload(payloads)) { 227 switch (getItemViewType(position)) { 228 case MESSAGE_TYPE_INCOMING_TEXT: 229 case MESSAGE_TYPE_INCOMING_MULTIMEDIA: 230 case MESSAGE_TYPE_OUTGOING_TEXT: 231 case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: 232 case MESSAGE_TYPE_UPDATE: 233 ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder; 234 if (payloads.contains(PAYLOAD_TIMESTAMP)) { 235 conversationViewHolder.getBindable().updateTimestamps(); 236 } 237 238 if (payloads.contains(PAYLOAD_NAME_COLORS)) { 239 conversationViewHolder.getBindable().updateContactNameColor(); 240 } 241 242 if (payloads.contains(PAYLOAD_SELECTED)) { 243 conversationViewHolder.getBindable().updateSelectedState(); 244 } 245 246 default: 247 return; 248 } 249 } else { 250 super.onBindViewHolder(holder, position, payloads); 251 } 252 } 253 254 public void setCondensedMode(ConversationItemDisplayMode condensedMode) { 255 this.displayMode = condensedMode; 256 notifyDataSetChanged(); 257 } 258 259 @Override 260 public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 261 switch (getItemViewType(position)) { 262 case MESSAGE_TYPE_INCOMING_TEXT: 263 case MESSAGE_TYPE_INCOMING_MULTIMEDIA: 264 case MESSAGE_TYPE_OUTGOING_TEXT: 265 case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: 266 case MESSAGE_TYPE_UPDATE: 267 ConversationViewHolder conversationViewHolder = (ConversationViewHolder) holder; 268 ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position)); 269 int adapterPosition = holder.getAdapterPosition(); 270 271 ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null; 272 ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null; 273 274 ConversationItemDisplayMode itemDisplayMode = displayMode != null ? displayMode : ConversationItemDisplayMode.Standard.INSTANCE; 275 276 conversationViewHolder.getBindable().bind(lifecycleOwner, 277 conversationMessage, 278 Optional.ofNullable(previousMessage != null ? previousMessage.getMessageRecord() : null), 279 Optional.ofNullable(nextMessage != null ? nextMessage.getMessageRecord() : null), 280 requestManager, 281 locale, 282 selected, 283 conversationMessage.getThreadRecipient(), 284 searchQuery, 285 conversationMessage == recordToPulse, 286 hasWallpaper && itemDisplayMode.displayWallpaper(), 287 isMessageRequestAccepted, 288 conversationMessage == inlineContent, 289 colorizer, 290 itemDisplayMode); 291 292 if (conversationMessage == recordToPulse) { 293 recordToPulse = null; 294 } 295 break; 296 case MESSAGE_TYPE_HEADER: 297 ((HeaderViewHolder) holder).bind(typingView); 298 break; 299 case MESSAGE_TYPE_FOOTER: 300 ((HeaderFooterViewHolder) holder).bind(footerView); 301 break; 302 } 303 } 304 305 @Override 306 public int getItemCount() { 307 boolean hasFooter = footerView != null; 308 return super.getItemCount() + (isTypingViewEnabled ? 1 : 0) + (hasFooter ? 1 : 0); 309 } 310 311 @Override 312 public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) { 313 if (holder instanceof ConversationViewHolder) { 314 ((ConversationViewHolder) holder).getBindable().unbind(); 315 } 316 } 317 318 @Override 319 public long getHeaderId(int position) { 320 if (isHeaderPosition(position)) return -1; 321 if (isFooterPosition(position)) return -1; 322 if (position >= getItemCount()) return -1; 323 if (position < 0) return -1; 324 325 ConversationMessage conversationMessage = getItem(position); 326 327 if (conversationMessage == null) return -1; 328 329 if (displayMode.getScheduleMessageMode()) { 330 calendar.setTimeInMillis(((MmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate()); 331 } else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) { 332 calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateSent()); 333 } else { 334 calendar.setTimeInMillis(conversationMessage.getConversationTimestamp()); 335 } 336 return calendar.get(Calendar.YEAR) * 1000L + calendar.get(Calendar.DAY_OF_YEAR); 337 } 338 339 @Override 340 public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) { 341 return new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_header, parent, false)); 342 } 343 344 @Override 345 public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position, int type) { 346 Context context = viewHolder.itemView.getContext(); 347 ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position)); 348 349 if (displayMode.getScheduleMessageMode()) { 350 viewHolder.setText(DateUtils.getScheduledMessagesDateHeaderString(viewHolder.itemView.getContext(), locale, ((MmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate())); 351 } else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) { 352 viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateSent())); 353 } else { 354 viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getConversationTimestamp())); 355 } 356 357 if (type == HEADER_TYPE_POPOVER_DATE) { 358 if (hasWallpaper) { 359 viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18); 360 } else { 361 viewHolder.setBackgroundRes(R.drawable.sticky_date_header_background); 362 } 363 } else if (type == HEADER_TYPE_INLINE_DATE) { 364 if (hasWallpaper) { 365 viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18); 366 } else { 367 viewHolder.clearBackground(); 368 } 369 } 370 371 if (hasWallpaper && ThemeUtil.isDarkTheme(context)) { 372 viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorNeutralInverse)); 373 } else { 374 viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)); 375 } 376 } 377 378 public @Nullable ConversationMessage getConversationMessage(int position) { 379 return getItem(position); 380 } 381 382 public @Nullable ConversationMessage getItem(int position) { 383 position = isTypingViewEnabled() ? position - 1 : position; 384 385 if (position < 0) { 386 return null; 387 } else { 388 if (pagingController != null) { 389 pagingController.onDataNeededAroundIndex(position); 390 } 391 392 if (position < super.getItemCount()) { 393 return super.getItem(position); 394 } else { 395 Log.d(TAG, "Could not access corrected position " + position + " as it is out of bounds."); 396 return null; 397 } 398 } 399 } 400 401 /** 402 * Checks a range around the given position for nulls. 403 * 404 * @param position The position we wish to jump to. 405 * @return true if we seem like we've paged in the right data, false if not so. 406 */ 407 public boolean canJumpToPosition(int position) { 408 position = isTypingViewEnabled() ? position - 1 : position; 409 if (position < 0) { 410 return false; 411 } 412 413 if (position > super.getItemCount()) { 414 Log.d(TAG, "Could not access corrected position " + position + " as it is out of bounds."); 415 return false; 416 } 417 418 int start = Math.max(position - 10, 0); 419 int end = Math.min(position + 5, super.getItemCount()); 420 421 for (int i = start; i < end; i++) { 422 if (super.getItem(i) == null) { 423 if (pagingController != null) { 424 pagingController.onDataNeededAroundIndex(position); 425 } 426 427 return false; 428 } 429 } 430 431 return true; 432 } 433 434 public void setPagingController(@Nullable PagingController pagingController) { 435 this.pagingController = pagingController; 436 } 437 438 public boolean isForRecipientId(@NonNull RecipientId recipientId) { 439 // TODO [alex] -- This should be fine, since we now have a 1:1 relationship between fragment and recipient. 440 return true; 441 } 442 443 void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, long unreadCount) { 444 viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (int) unreadCount, (int) unreadCount)); 445 446 if (hasWallpaper) { 447 viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_18); 448 viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.transparent_black_80)); 449 } else { 450 viewHolder.clearBackground(); 451 viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.core_grey_45)); 452 } 453 } 454 455 public boolean hasNoConversationMessages() { 456 return super.getItemCount() == 0; 457 } 458 459 /** 460 * The presence of a header may throw off the position you'd like to jump to. This will return 461 * an adjusted message position based on adapter state. 462 */ 463 @MainThread 464 public int getAdapterPositionForMessagePosition(int messagePosition) { 465 return isTypingViewEnabled() ? messagePosition + 1 : messagePosition; 466 } 467 468 /** 469 * Finds the received timestamp for the item at the requested adapter position. Will return 0 if 470 * the position doesn't refer to an incoming message. 471 */ 472 @MainThread 473 long getReceivedTimestamp(int position) { 474 if (isHeaderPosition(position)) return 0; 475 if (isFooterPosition(position)) return 0; 476 if (position >= getItemCount()) return 0; 477 if (position < 0) return 0; 478 479 ConversationMessage conversationMessage = getItem(position); 480 481 if (conversationMessage == null || conversationMessage.getMessageRecord().isOutgoing()) { 482 return 0; 483 } else { 484 return conversationMessage.getMessageRecord().getDateReceived(); 485 } 486 } 487 488 /** 489 * Sets the view the appears at the top of the list (because the list is reversed). 490 */ 491 void setFooterView(@Nullable View view) { 492 boolean hadFooter = hasFooter(); 493 494 this.footerView = view; 495 496 if (view == null && hadFooter) { 497 notifyItemRemoved(getItemCount()); 498 } else if (view != null && hadFooter) { 499 notifyItemChanged(getItemCount() - 1); 500 } else if (view != null) { 501 notifyItemInserted(getItemCount() - 1); 502 } 503 } 504 505 /** 506 * Sets the view that appears at the bottom of the list (because the list is reversed). 507 */ 508 void setTypingView(@NonNull View view) { 509 this.typingView = view; 510 } 511 512 void setTypingViewEnabled(boolean isTypingViewEnabled) { 513 if (typingView == null && isTypingViewEnabled) { 514 throw new IllegalStateException("Must set header before enabling."); 515 } 516 517 if (this.isTypingViewEnabled && !isTypingViewEnabled) { 518 this.isTypingViewEnabled = false; 519 notifyItemRemoved(0); 520 } else if (this.isTypingViewEnabled) { 521 notifyItemChanged(0); 522 } else if (isTypingViewEnabled) { 523 this.isTypingViewEnabled = true; 524 notifyItemInserted(0); 525 } 526 } 527 528 /** 529 * Momentarily highlights a mention at the requested position. 530 */ 531 public void pulseAtPosition(int position) { 532 if (position >= 0 && position < getItemCount()) { 533 int correctedPosition = isHeaderPosition(position) ? position + 1 : position; 534 535 recordToPulse = getItem(correctedPosition); 536 pulseRequest = new PulseRequest(position, recordToPulse.getMessageRecord().isOutgoing()); 537 notifyItemChanged(correctedPosition); 538 } 539 } 540 541 @Nullable 542 public PulseRequest consumePulseRequest() { 543 PulseRequest request = pulseRequest; 544 pulseRequest = null; 545 return request; 546 } 547 548 /** 549 * Conversation search query updated. Allows rendering of text highlighting. 550 */ 551 void onSearchQueryUpdated(String query) { 552 if (!Objects.equals(query, this.searchQuery)) { 553 this.searchQuery = query; 554 notifyDataSetChanged(); 555 } 556 } 557 558 /** 559 * Lets the adapter know that the wallpaper state has changed. 560 * @return True if the internal wallpaper state changed, otherwise false. 561 */ 562 public boolean onHasWallpaperChanged(boolean hasWallpaper) { 563 if (this.hasWallpaper != hasWallpaper) { 564 Log.d(TAG, "Resetting adapter due to wallpaper change."); 565 this.hasWallpaper = hasWallpaper; 566 notifyDataSetChanged(); 567 return true; 568 } else { 569 return false; 570 } 571 } 572 573 /** 574 * Returns set of records that are selected in multi-select mode. 575 */ 576 public Set<MultiselectPart> getSelectedItems() { 577 return new HashSet<>(selected); 578 } 579 580 public void removeFromSelection(@NonNull Set<MultiselectPart> parts) { 581 selected.removeAll(parts); 582 updateSelected(); 583 } 584 585 /** 586 * Clears all selected records from multi-select mode. 587 */ 588 void clearSelection() { 589 selected.clear(); 590 updateSelected(); 591 } 592 593 /** 594 * Toggles the selected state of a record in multi-select mode. 595 */ 596 void toggleSelection(MultiselectPart multiselectPart) { 597 if (selected.contains(multiselectPart)) { 598 selected.remove(multiselectPart); 599 } else { 600 selected.add(multiselectPart); 601 } 602 updateSelected(); 603 } 604 605 private void updateSelected() { 606 notifyItemRangeChanged(0, getItemCount(), PAYLOAD_SELECTED); 607 } 608 609 /** 610 * Provided a pool, this will initialize it with view counts that make sense. 611 */ 612 @MainThread 613 public static void initializePool(@NonNull RecyclerView.RecycledViewPool pool) { 614 pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_TEXT, 25); 615 pool.setMaxRecycledViews(MESSAGE_TYPE_INCOMING_MULTIMEDIA, 15); 616 pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_TEXT, 25); 617 pool.setMaxRecycledViews(MESSAGE_TYPE_OUTGOING_MULTIMEDIA, 15); 618 pool.setMaxRecycledViews(MESSAGE_TYPE_PLACEHOLDER, 15); 619 pool.setMaxRecycledViews(MESSAGE_TYPE_HEADER, 1); 620 pool.setMaxRecycledViews(MESSAGE_TYPE_FOOTER, 1); 621 pool.setMaxRecycledViews(MESSAGE_TYPE_UPDATE, 5); 622 } 623 624 public boolean isTypingViewEnabled() { 625 return isTypingViewEnabled; 626 } 627 628 public boolean hasFooter() { 629 return footerView != null; 630 } 631 632 private boolean isHeaderPosition(int position) { 633 return isTypingViewEnabled() && position == 0; 634 } 635 636 private boolean isFooterPosition(int position) { 637 return hasFooter() && position == (getItemCount() - 1); 638 } 639 640 private static @LayoutRes int getLayoutForViewType(int viewType) { 641 switch (viewType) { 642 case MESSAGE_TYPE_OUTGOING_TEXT: return R.layout.conversation_item_sent_text_only; 643 case MESSAGE_TYPE_OUTGOING_MULTIMEDIA: return R.layout.conversation_item_sent_multimedia; 644 case MESSAGE_TYPE_INCOMING_TEXT: return R.layout.conversation_item_received_text_only; 645 case MESSAGE_TYPE_INCOMING_MULTIMEDIA: return R.layout.conversation_item_received_multimedia; 646 case MESSAGE_TYPE_UPDATE: return R.layout.conversation_item_update; 647 default: throw new IllegalArgumentException("Unknown type!"); 648 } 649 } 650 651 public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) { 652 try { 653 return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0)); 654 } catch (IndexOutOfBoundsException e) { 655 Log.w(TAG, "Race condition changed size of conversation", e); 656 return null; 657 } 658 } 659 660 public void setMessageRequestAccepted(boolean messageRequestAccepted) { 661 if (this.isMessageRequestAccepted != messageRequestAccepted) { 662 this.isMessageRequestAccepted = messageRequestAccepted; 663 notifyDataSetChanged(); 664 } 665 } 666 667 public void playInlineContent(@Nullable ConversationMessage conversationMessage) { 668 if (this.inlineContent != conversationMessage) { 669 this.inlineContent = conversationMessage; 670 notifyDataSetChanged(); 671 } 672 } 673 674 public void updateTimestamps() { 675 notifyItemRangeChanged(0, getItemCount(), PAYLOAD_TIMESTAMP); 676 } 677 678 final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable { 679 public ConversationViewHolder(final @NonNull View itemView) { 680 super(itemView); 681 } 682 683 public BindableConversationItem getBindable() { 684 return (BindableConversationItem) itemView; 685 } 686 687 @Override 688 public void showProjectionArea() { 689 getBindable().showProjectionArea(); 690 } 691 692 @Override 693 public void hideProjectionArea() { 694 getBindable().hideProjectionArea(); 695 } 696 697 @Override 698 public @Nullable MediaItem getMediaItem() { 699 return getBindable().getMediaItem(); 700 } 701 702 @Override 703 public @Nullable GiphyMp4PlaybackPolicyEnforcer getPlaybackPolicyEnforcer() { 704 return getBindable().getPlaybackPolicyEnforcer(); 705 } 706 707 @Override 708 public @NonNull Projection getGiphyMp4PlayableProjection(@NonNull ViewGroup recyclerView) { 709 return getBindable().getGiphyMp4PlayableProjection(recyclerView); 710 } 711 712 @Override 713 public boolean canPlayContent() { 714 return getBindable().canPlayContent(); 715 } 716 717 @Override 718 public boolean shouldProjectContent() { 719 return getBindable().shouldProjectContent(); 720 } 721 722 @Override 723 public @NonNull ProjectionList getColorizerProjections(@NonNull ViewGroup coordinateRoot) { 724 return getBindable().getColorizerProjections(coordinateRoot); 725 } 726 } 727 728 static class StickyHeaderViewHolder extends RecyclerView.ViewHolder { 729 TextView textView; 730 View divider; 731 732 StickyHeaderViewHolder(View itemView) { 733 super(itemView); 734 textView = itemView.findViewById(R.id.text); 735 divider = itemView.findViewById(R.id.last_seen_divider); 736 } 737 738 StickyHeaderViewHolder(TextView textView) { 739 super(textView); 740 this.textView = textView; 741 } 742 743 public void setText(CharSequence text) { 744 textView.setText(text); 745 } 746 747 public void setTextColor(@ColorInt int color) { 748 textView.setTextColor(color); 749 } 750 751 public void setBackgroundRes(@DrawableRes int resId) { 752 textView.setBackgroundResource(resId); 753 } 754 755 public void setDividerColor(@ColorInt int color) { 756 if (divider != null) { 757 divider.setBackgroundColor(color); 758 } 759 } 760 761 public void clearBackground() { 762 textView.setBackground(null); 763 } 764 } 765 766 public abstract static class HeaderFooterViewHolder extends RecyclerView.ViewHolder { 767 768 private ViewGroup container; 769 770 HeaderFooterViewHolder(@NonNull View itemView) { 771 super(itemView); 772 this.container = (ViewGroup) itemView; 773 } 774 775 void bind(@Nullable View view) { 776 unbind(); 777 778 if (view != null) { 779 removeViewFromParent(view); 780 container.addView(view); 781 } 782 } 783 784 void unbind() { 785 container.removeAllViews(); 786 } 787 788 private void removeViewFromParent(@NonNull View view) { 789 if (view.getParent() != null) { 790 ((ViewGroup) view.getParent()).removeView(view); 791 } 792 } 793 } 794 795 public static class FooterViewHolder extends HeaderFooterViewHolder { 796 FooterViewHolder(@NonNull View itemView) { 797 super(itemView); 798 setPaddingTop(); 799 } 800 801 @Override 802 void bind(@Nullable View view) { 803 super.bind(view); 804 setPaddingTop(); 805 } 806 807 private void setPaddingTop() { 808 if (Build.VERSION.SDK_INT <= 23) { 809 int addToPadding = ViewUtil.getStatusBarHeight(itemView) + (int) ThemeUtil.getThemedDimen(itemView.getContext(), android.R.attr.actionBarSize); 810 ViewUtil.setPaddingTop(itemView, itemView.getPaddingTop() + addToPadding); 811 } 812 } 813 } 814 815 public static class HeaderViewHolder extends HeaderFooterViewHolder { 816 HeaderViewHolder(@NonNull View itemView) { 817 super(itemView); 818 } 819 } 820 821 private static class PlaceholderViewHolder extends RecyclerView.ViewHolder { 822 PlaceholderViewHolder(@NonNull View itemView) { 823 super(itemView); 824 } 825 } 826 827 public interface ItemClickListener extends BindableConversationItem.EventListener { 828 void onItemClick(MultiselectPart item); 829 void onItemLongClick(View itemView, MultiselectPart item); 830 } 831}