Add ConversationAdapterV2.
This commit is contained in:
parent
a1eb33b1f6
commit
65d5f4c426
21 changed files with 660 additions and 92 deletions
6
.idea/copyright/Signal.xml
generated
Normal file
6
.idea/copyright/Signal.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright &#36;today.year Signal Messenger, LLC SPDX-License-Identifier: AGPL-3.0-only" />
|
||||
<option name="myName" value="Signal" />
|
||||
</copyright>
|
||||
</component>
|
7
.idea/copyright/profiles_settings.xml
generated
Normal file
7
.idea/copyright/profiles_settings.xml
generated
Normal file
|
@ -0,0 +1,7 @@
|
|||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="All" copyright="Signal" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
9
.idea/fileTemplates/internal/AnnotationType.java
generated
Normal file
9
.idea/fileTemplates/internal/AnnotationType.java
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public @interface ${NAME} {
|
||||
}
|
9
.idea/fileTemplates/internal/Class.java
generated
Normal file
9
.idea/fileTemplates/internal/Class.java
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public class ${NAME} {
|
||||
}
|
9
.idea/fileTemplates/internal/Enum.java
generated
Normal file
9
.idea/fileTemplates/internal/Enum.java
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public enum ${NAME} {
|
||||
}
|
9
.idea/fileTemplates/internal/Interface.java
generated
Normal file
9
.idea/fileTemplates/internal/Interface.java
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public interface ${NAME} {
|
||||
}
|
11
.idea/fileTemplates/internal/Kotlin Class.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Class.kt
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
class ${NAME} {
|
||||
}
|
11
.idea/fileTemplates/internal/Kotlin Enum.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Enum.kt
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
enum class ${NAME} {
|
||||
}
|
9
.idea/fileTemplates/internal/Kotlin File.kt
generated
Normal file
9
.idea/fileTemplates/internal/Kotlin File.kt
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
11
.idea/fileTemplates/internal/Kotlin Interface.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Interface.kt
generated
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
interface ${NAME} {
|
||||
}
|
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
|
@ -78,7 +79,8 @@ import java.util.Set;
|
|||
*/
|
||||
public class ConversationAdapter
|
||||
extends ListAdapter<ConversationMessage, RecyclerView.ViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>
|
||||
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationAdapter.StickyHeaderViewHolder>,
|
||||
ConversationAdapterBridge
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(ConversationAdapter.class);
|
||||
|
@ -380,6 +382,10 @@ public class ConversationAdapter
|
|||
}
|
||||
}
|
||||
|
||||
public @Nullable ConversationMessage getConversationMessage(int position) {
|
||||
return getItem(position);
|
||||
}
|
||||
|
||||
public @Nullable ConversationMessage getItem(int position) {
|
||||
position = isTypingViewEnabled() ? position - 1 : position;
|
||||
|
||||
|
@ -453,7 +459,7 @@ public class ConversationAdapter
|
|||
}
|
||||
}
|
||||
|
||||
boolean hasNoConversationMessages() {
|
||||
public boolean hasNoConversationMessages() {
|
||||
return super.getItemCount() == 0;
|
||||
}
|
||||
|
||||
|
@ -825,37 +831,6 @@ public class ConversationAdapter
|
|||
}
|
||||
}
|
||||
|
||||
public static class PulseRequest {
|
||||
private final int position;
|
||||
private final boolean isOutgoing;
|
||||
|
||||
PulseRequest(int position, boolean isOutgoing) {
|
||||
this.position = position;
|
||||
this.isOutgoing = isOutgoing;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public boolean isOutgoing() {
|
||||
return isOutgoing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
final PulseRequest that = (PulseRequest) o;
|
||||
return position == that.position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(position);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ItemClickListener extends BindableConversationItem.EventListener {
|
||||
void onItemClick(MultiselectPart item);
|
||||
void onItemLongClick(View itemView, MultiselectPart item);
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
|
||||
/**
|
||||
* Temporary shared interface between the two conversation adapters strictly for use in
|
||||
* shared decorators and other utils.
|
||||
*/
|
||||
interface ConversationAdapterBridge {
|
||||
fun hasNoConversationMessages(): Boolean
|
||||
fun getConversationMessage(position: Int): ConversationMessage?
|
||||
fun consumePulseRequest(): PulseRequest?
|
||||
|
||||
val selectedItems: Set<MultiselectPart>
|
||||
|
||||
data class PulseRequest(val position: Int, val isOutgoing: Boolean)
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.view.Menu
|
||||
|
@ -8,6 +13,7 @@ import androidx.core.view.MenuProvider
|
|||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -18,6 +24,8 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
*/
|
||||
internal object ConversationOptionsMenu {
|
||||
|
||||
private val TAG = Log.tag(ConversationOptionsMenu::class.java)
|
||||
|
||||
/**
|
||||
* MenuProvider implementation for the conversation options menu.
|
||||
*/
|
||||
|
@ -43,7 +51,12 @@ internal object ConversationOptionsMenu {
|
|||
isInBubble
|
||||
) = callback.getSnapshot()
|
||||
|
||||
if (isInMessageRequest && (recipient != null) && !recipient.isBlocked) {
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "Recipient is null, no menu")
|
||||
return
|
||||
}
|
||||
|
||||
if (isInMessageRequest && !recipient.isBlocked) {
|
||||
if (isActiveGroup) {
|
||||
menuInflater.inflate(R.menu.conversation_message_requests_group, menu)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -5,12 +10,12 @@ import android.content.Context;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
|
@ -72,8 +77,8 @@ public class MarkReadHelper {
|
|||
* @return A Present(Long) if there's a timestamp to proceed with, or Empty if this request should be ignored.
|
||||
*/
|
||||
@SuppressWarnings("resource")
|
||||
public static @NonNull Optional<Long> getLatestTimestamp(@NonNull ConversationAdapter conversationAdapter,
|
||||
@NonNull SmoothScrollingLinearLayoutManager layoutManager)
|
||||
public static @NonNull Optional<Long> getLatestTimestamp(@NonNull ConversationAdapterBridge conversationAdapter,
|
||||
@NonNull LinearLayoutManager layoutManager)
|
||||
{
|
||||
if (conversationAdapter.hasNoConversationMessages()) {
|
||||
return Optional.empty();
|
||||
|
@ -84,9 +89,9 @@ public class MarkReadHelper {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
ConversationMessage item = conversationAdapter.getItem(position);
|
||||
ConversationMessage item = conversationAdapter.getConversationMessage(position);
|
||||
if (item == null) {
|
||||
item = conversationAdapter.getItem(position + 1);
|
||||
item = conversationAdapter.getConversationMessage(position + 1);
|
||||
}
|
||||
|
||||
if (item != null) {
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect
|
||||
|
||||
import android.animation.Animator
|
||||
|
@ -27,8 +32,8 @@ import com.airbnb.lottie.SimpleColorFilter
|
|||
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||
import org.signal.core.util.SetUtil
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.PulseRequest
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
@ -118,7 +123,7 @@ class MultiselectItemDecoration(
|
|||
}
|
||||
|
||||
private fun getCurrentSelection(parent: RecyclerView): Set<MultiselectPart> {
|
||||
return (parent.adapter as ConversationAdapter).selectedItems
|
||||
return (parent.adapter as ConversationAdapterBridge).selectedItems
|
||||
}
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
|
@ -150,7 +155,7 @@ class MultiselectItemDecoration(
|
|||
outRect.setEmpty()
|
||||
updateChildOffsets(parent, view)
|
||||
|
||||
consumePulseRequest(parent.adapter as ConversationAdapter)
|
||||
consumePulseRequest(parent.adapter as ConversationAdapterBridge)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,7 +163,7 @@ class MultiselectItemDecoration(
|
|||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val adapter = parent.adapter as ConversationAdapter
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
|
||||
if (adapter.selectedItems.isEmpty()) {
|
||||
drawFocusShadeUnderIfNecessary(canvas, parent)
|
||||
|
@ -221,7 +226,7 @@ class MultiselectItemDecoration(
|
|||
* Draws the selected check or empty circle.
|
||||
*/
|
||||
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val adapter = parent.adapter as ConversationAdapter
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
if (adapter.selectedItems.isEmpty()) {
|
||||
drawFocusShadeOverIfNecessary(canvas, parent)
|
||||
}
|
||||
|
@ -232,7 +237,7 @@ class MultiselectItemDecoration(
|
|||
invalidateIfEnterExitAnimatorsAreRunning(parent)
|
||||
}
|
||||
|
||||
private fun drawChecks(parent: RecyclerView, canvas: Canvas, adapter: ConversationAdapter) {
|
||||
private fun drawChecks(parent: RecyclerView, canvas: Canvas, adapter: ConversationAdapterBridge) {
|
||||
val drawCircleBehindSelector = chatWallpaperProvider()?.isPhoto == true
|
||||
val multiselectChildren: Sequence<Multiselectable> = parent.children.filterIsInstance(Multiselectable::class.java)
|
||||
|
||||
|
@ -337,7 +342,7 @@ class MultiselectItemDecoration(
|
|||
* called in getItemOffsets to ensure the gutter goes away when multiselect mode ends.
|
||||
*/
|
||||
private fun updateChildOffsets(parent: RecyclerView, child: View) {
|
||||
val adapter = parent.adapter as ConversationAdapter
|
||||
val adapter = parent.adapter as ConversationAdapterBridge
|
||||
val isLtr = ViewUtil.isLtr(child)
|
||||
|
||||
val isAnimatingSelection = enterExitAnimation != null && isInitialAnimation()
|
||||
|
@ -542,8 +547,8 @@ class MultiselectItemDecoration(
|
|||
}
|
||||
}
|
||||
|
||||
private fun consumePulseRequest(adapter: ConversationAdapter) {
|
||||
val pulseRequest = adapter.consumePulseRequest()
|
||||
private fun consumePulseRequest(adapter: ConversationAdapterBridge) {
|
||||
val pulseRequest: PulseRequest? = adapter.consumePulseRequest()
|
||||
if (pulseRequest != null) {
|
||||
val pulseColor = if (pulseRequest.isOutgoing) pulseOutgoingColor else pulseIncomingColor
|
||||
pulseRequestAnimators[pulseRequest]?.cancel()
|
||||
|
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.toOptional
|
||||
import org.thoughtcrime.securesms.BindableConversationItem
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationUpdate
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingMedia
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingMedia
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.ProjectionList
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
|
||||
class ConversationAdapterV2(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val glideRequests: GlideRequests,
|
||||
private val clickListener: ConversationAdapter.ItemClickListener,
|
||||
private var hasWallpaper: Boolean,
|
||||
private val colorizer: Colorizer
|
||||
) : PagingMappingAdapter<ConversationElementKey>(), ConversationAdapterBridge {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationAdapterV2::class.java)
|
||||
}
|
||||
|
||||
private val _selected = hashSetOf<MultiselectPart>()
|
||||
|
||||
override val selectedItems: Set<MultiselectPart>
|
||||
get() = _selected.toSet()
|
||||
|
||||
private var searchQuery: String? = null
|
||||
private var inlineContent: ConversationMessage? = null
|
||||
|
||||
private var recordToPulse: ConversationMessage? = null
|
||||
private var pulseRequest: ConversationAdapterBridge.PulseRequest? = null
|
||||
|
||||
private val condensedMode: ConversationItemDisplayMode? = null
|
||||
|
||||
init {
|
||||
registerFactory(ConversationUpdate::class.java) { parent ->
|
||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_update, parent, false)
|
||||
ConversationUpdateViewHolder(view)
|
||||
}
|
||||
|
||||
registerFactory(OutgoingTextOnly::class.java) { parent ->
|
||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_sent_text_only, parent, false)
|
||||
OutgoingTextOnlyViewHolder(view)
|
||||
}
|
||||
|
||||
registerFactory(OutgoingMedia::class.java) { parent ->
|
||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_sent_multimedia, parent, false)
|
||||
OutgoingMediaViewHolder(view)
|
||||
}
|
||||
|
||||
registerFactory(IncomingTextOnly::class.java) { parent ->
|
||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_received_text_only, parent, false)
|
||||
IncomingTextOnlyViewHolder(view)
|
||||
}
|
||||
|
||||
registerFactory(IncomingMedia::class.java) { parent ->
|
||||
val view = CachedInflater.from(parent.context).inflate<View>(R.layout.conversation_item_received_multimedia, parent, false)
|
||||
IncomingMediaViewHolder(view)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAdapterPositionForMessagePosition(startPosition: Int): Int {
|
||||
return startPosition - 1
|
||||
}
|
||||
|
||||
fun getLastVisibleConversationMessage(position: Int): ConversationMessage? {
|
||||
return try {
|
||||
// todo [cody] handle conversation banner adjustment
|
||||
getConversationMessage(position)
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
Log.w(TAG, "Race condition changed size of conversation", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun canJumpToPosition(absolutePosition: Int): Boolean {
|
||||
// todo [cody] handle typing indicator
|
||||
val position = absolutePosition
|
||||
|
||||
if (position < 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (position > super.getItemCount()) {
|
||||
Log.d(TAG, "Could not access corrected position $position as it is out of bounds.")
|
||||
return false
|
||||
}
|
||||
|
||||
return isRangeAvailable(position - 10, position + 5)
|
||||
}
|
||||
|
||||
fun playInlineContent(conversationMessage: ConversationMessage?) {
|
||||
if (this.inlineContent !== conversationMessage) {
|
||||
this.inlineContent = conversationMessage
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getConversationMessage(position: Int): ConversationMessage? {
|
||||
return when (val item = getItem(position)) {
|
||||
is ConversationMessageElement -> item.conversationMessage
|
||||
null -> null
|
||||
else -> throw AssertionError("Invalid item: ${item.javaClass}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasNoConversationMessages(): Boolean {
|
||||
return itemCount == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Momentarily highlights a mention at the requested position.
|
||||
*/
|
||||
fun pulseAtPosition(position: Int) {
|
||||
if (position >= 0 && position < itemCount) {
|
||||
// todo [cody] adjust for typing indicator
|
||||
val correctedPosition = position
|
||||
|
||||
recordToPulse = getConversationMessage(correctedPosition)
|
||||
if (recordToPulse != null) {
|
||||
pulseRequest = ConversationAdapterBridge.PulseRequest(position, recordToPulse!!.messageRecord.isOutgoing)
|
||||
}
|
||||
notifyItemChanged(correctedPosition)
|
||||
}
|
||||
}
|
||||
|
||||
override fun consumePulseRequest(): ConversationAdapterBridge.PulseRequest? {
|
||||
val request = pulseRequest
|
||||
pulseRequest = null
|
||||
return request
|
||||
}
|
||||
|
||||
fun onHasWallpaperChanged(hasChanged: Boolean) {
|
||||
// todo [cody] implement
|
||||
}
|
||||
|
||||
private inner class ConversationUpdateViewHolder(itemView: View) : ConversationViewHolder<ConversationUpdate>(itemView) {
|
||||
override fun bind(model: ConversationUpdate) {
|
||||
bindable.setEventListener(clickListener)
|
||||
bindable.bind(
|
||||
lifecycleOwner,
|
||||
model.conversationMessage,
|
||||
previousMessage,
|
||||
nextMessage,
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
_selected,
|
||||
model.conversationMessage.threadRecipient,
|
||||
searchQuery,
|
||||
false,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
true, // isMessageRequestAccepted,
|
||||
model.conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
displayMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OutgoingTextOnlyViewHolder(itemView: View) : ConversationViewHolder<OutgoingTextOnly>(itemView) {
|
||||
override fun bind(model: OutgoingTextOnly) {
|
||||
bindable.setEventListener(clickListener)
|
||||
bindable.bind(
|
||||
lifecycleOwner,
|
||||
model.conversationMessage,
|
||||
previousMessage,
|
||||
nextMessage,
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
_selected,
|
||||
model.conversationMessage.threadRecipient,
|
||||
searchQuery,
|
||||
false,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
true, // isMessageRequestAccepted,
|
||||
model.conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
displayMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OutgoingMediaViewHolder(itemView: View) : ConversationViewHolder<OutgoingMedia>(itemView) {
|
||||
override fun bind(model: OutgoingMedia) {
|
||||
bindable.setEventListener(clickListener)
|
||||
bindable.bind(
|
||||
lifecycleOwner,
|
||||
model.conversationMessage,
|
||||
previousMessage,
|
||||
nextMessage,
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
_selected,
|
||||
model.conversationMessage.threadRecipient,
|
||||
searchQuery,
|
||||
false,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
true, // isMessageRequestAccepted,
|
||||
model.conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
displayMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class IncomingTextOnlyViewHolder(itemView: View) : ConversationViewHolder<IncomingTextOnly>(itemView) {
|
||||
override fun bind(model: IncomingTextOnly) {
|
||||
bindable.setEventListener(clickListener)
|
||||
bindable.bind(
|
||||
lifecycleOwner,
|
||||
model.conversationMessage,
|
||||
previousMessage,
|
||||
nextMessage,
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
_selected,
|
||||
model.conversationMessage.threadRecipient,
|
||||
searchQuery,
|
||||
false,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
true, // isMessageRequestAccepted,
|
||||
model.conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
displayMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class IncomingMediaViewHolder(itemView: View) : ConversationViewHolder<IncomingMedia>(itemView) {
|
||||
override fun bind(model: IncomingMedia) {
|
||||
bindable.setEventListener(clickListener)
|
||||
bindable.bind(
|
||||
lifecycleOwner,
|
||||
model.conversationMessage,
|
||||
previousMessage,
|
||||
nextMessage,
|
||||
glideRequests,
|
||||
Locale.getDefault(),
|
||||
_selected,
|
||||
model.conversationMessage.threadRecipient,
|
||||
searchQuery,
|
||||
false,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
true, // isMessageRequestAccepted,
|
||||
model.conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
displayMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private abstract inner class ConversationViewHolder<T>(itemView: View) : MappingViewHolder<T>(itemView), GiphyMp4Playable, Colorizable {
|
||||
val bindable: BindableConversationItem
|
||||
get() = itemView as BindableConversationItem
|
||||
|
||||
protected val previousMessage: Optional<MessageRecord>
|
||||
get() = getConversationMessage(bindingAdapterPosition + 1)?.messageRecord.toOptional()
|
||||
|
||||
protected val nextMessage: Optional<MessageRecord>
|
||||
get() = getConversationMessage(bindingAdapterPosition - 1)?.messageRecord.toOptional()
|
||||
|
||||
protected val displayMode: ConversationItemDisplayMode
|
||||
get() = condensedMode ?: ConversationItemDisplayMode.STANDARD
|
||||
|
||||
override fun showProjectionArea() {
|
||||
bindable.showProjectionArea()
|
||||
}
|
||||
|
||||
override fun hideProjectionArea() {
|
||||
bindable.hideProjectionArea()
|
||||
}
|
||||
|
||||
override fun getMediaItem(): MediaItem? {
|
||||
return bindable.mediaItem
|
||||
}
|
||||
|
||||
override fun getPlaybackPolicyEnforcer(): GiphyMp4PlaybackPolicyEnforcer? {
|
||||
return bindable.playbackPolicyEnforcer
|
||||
}
|
||||
|
||||
override fun getGiphyMp4PlayableProjection(recyclerView: ViewGroup): Projection {
|
||||
return bindable.getGiphyMp4PlayableProjection(recyclerView)
|
||||
}
|
||||
|
||||
override fun canPlayContent(): Boolean {
|
||||
return bindable.canPlayContent()
|
||||
}
|
||||
|
||||
override fun shouldProjectContent(): Boolean {
|
||||
return bindable.shouldProjectContent()
|
||||
}
|
||||
|
||||
override fun getColorizerProjections(coordinateRoot: ViewGroup): ProjectionList {
|
||||
return bindable.getColorizerProjections(coordinateRoot)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
|
@ -169,12 +174,12 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
private val colorizer = Colorizer()
|
||||
|
||||
private lateinit var conversationOptionsMenuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var layoutManager: SmoothScrollingLinearLayoutManager
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var markReadHelper: MarkReadHelper
|
||||
private lateinit var giphyMp4ProjectionRecycler: GiphyMp4ProjectionRecycler
|
||||
private lateinit var addToContactsLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
private lateinit var adapter: ConversationAdapterV2
|
||||
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
|
||||
|
||||
private var animationsAllowed = false
|
||||
|
@ -434,14 +439,12 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
binding.conversationItemRecycler.layoutManager = layoutManager
|
||||
binding.conversationItemRecycler.addOnScrollListener(ScrollListener())
|
||||
|
||||
adapter = ConversationAdapter(
|
||||
requireContext(),
|
||||
viewLifecycleOwner,
|
||||
GlideApp.with(this),
|
||||
Locale.getDefault(),
|
||||
ConversationItemClickListener(),
|
||||
args.wallpaper != null,
|
||||
colorizer
|
||||
adapter = ConversationAdapterV2(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
glideRequests = GlideApp.with(this),
|
||||
clickListener = ConversationItemClickListener(),
|
||||
hasWallpaper = args.wallpaper != null,
|
||||
colorizer = colorizer
|
||||
)
|
||||
|
||||
scrollToPositionDelegate = ScrollToPositionDelegate(
|
||||
|
@ -500,7 +503,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
return callback
|
||||
}
|
||||
|
||||
private fun toast(@StringRes toastTextId: Int, toastDuration: Int) {
|
||||
private fun toast(@StringRes toastTextId: Int, toastDuration: Int = Toast.LENGTH_SHORT) {
|
||||
ThreadUtil.runOnMain {
|
||||
if (context != null) {
|
||||
Toast.makeText(context, toastTextId, toastDuration).show()
|
||||
|
@ -570,7 +573,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
|
||||
if (quote.isOriginalMissing) {
|
||||
Log.i(TAG, "onQuoteClicked: Original message is missing.")
|
||||
toast(R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT)
|
||||
toast(R.string.ConversationFragment_quoted_message_not_found)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -594,7 +597,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
if (it >= 0) {
|
||||
moveToPosition(it)
|
||||
} else {
|
||||
toast(R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT)
|
||||
toast(R.string.ConversationFragment_quoted_message_no_longer_available)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -998,8 +1001,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
|||
}
|
||||
|
||||
private class LastSeenPositionUpdater(
|
||||
val adapter: ConversationAdapter,
|
||||
val layoutManager: SmoothScrollingLinearLayoutManager,
|
||||
val adapter: ConversationAdapterV2,
|
||||
val layoutManager: LinearLayoutManager,
|
||||
val viewModel: ConversationViewModel
|
||||
) : DefaultLifecycleObserver {
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import org.signal.paging.ObservablePagedData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
/**
|
||||
* Represents the content that will be displayed in the conversation
|
||||
* thread (recycler).
|
||||
*/
|
||||
class ConversationThreadState(
|
||||
val items: ObservablePagedData<MessageId, ConversationMessage>,
|
||||
val items: ObservablePagedData<ConversationElementKey, MappingModel<*>>,
|
||||
val meta: ConversationData
|
||||
)
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
@ -17,8 +22,8 @@ import org.signal.paging.ProxyPagingController
|
|||
import org.thoughtcrime.securesms.conversation.ConversationIntents.Args
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
@ -57,7 +62,7 @@ class ConversationViewModel(
|
|||
.onBackpressureBuffer()
|
||||
.distinct()
|
||||
|
||||
val pagingController = ProxyPagingController<MessageId>()
|
||||
val pagingController = ProxyPagingController<ConversationElementKey>()
|
||||
|
||||
val nameColorsMap: Observable<Map<RecipientId, NameColor>> = _recipient.flatMap { repository.getNameColorsMap(it, groupAuthorNameColorHelper) }
|
||||
|
||||
|
@ -84,10 +89,10 @@ class ConversationViewModel(
|
|||
Observable.create<Unit> { emitter ->
|
||||
val controller = threadState.items.controller
|
||||
val messageUpdateObserver = DatabaseObserver.MessageObserver {
|
||||
controller.onDataItemChanged(it)
|
||||
controller.onDataItemChanged(ConversationElementKey.forMessage(it.id))
|
||||
}
|
||||
val messageInsertObserver = DatabaseObserver.MessageObserver {
|
||||
controller.onDataItemInserted(it, 0)
|
||||
controller.onDataItemInserted(ConversationElementKey.forMessage(it.id), 0)
|
||||
}
|
||||
val conversationObserver = DatabaseObserver.Observer {
|
||||
controller.onDataInvalidated()
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.data
|
||||
|
||||
import android.content.Context
|
||||
|
@ -20,8 +25,19 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
|
||||
private typealias ConversationElement = MappingModel<*>
|
||||
|
||||
sealed interface ConversationElementKey {
|
||||
companion object {
|
||||
fun forMessage(id: Long): ConversationElementKey = MessageBackedKey(id)
|
||||
}
|
||||
}
|
||||
|
||||
private data class MessageBackedKey(val id: Long) : ConversationElementKey
|
||||
|
||||
/**
|
||||
* ConversationDataSource for V2. Assumes that ThreadId is never -1L.
|
||||
*/
|
||||
|
@ -31,16 +47,16 @@ class ConversationDataSource(
|
|||
private val messageRequestData: ConversationData.MessageRequestData,
|
||||
private val showUniversalExpireTimerUpdate: Boolean,
|
||||
private var baseSize: Int
|
||||
) : PagedDataSource<MessageId, ConversationMessage> {
|
||||
|
||||
init {
|
||||
check(threadId > 0)
|
||||
}
|
||||
) : PagedDataSource<ConversationElementKey, ConversationElement> {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationDataSource::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
check(threadId > 0)
|
||||
}
|
||||
|
||||
private val threadRecipient: Recipient by lazy {
|
||||
SignalDatabase.threads.getRecipientForThreadId(threadId)!!
|
||||
}
|
||||
|
@ -69,7 +85,7 @@ class ConversationDataSource(
|
|||
return SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
}
|
||||
|
||||
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<ConversationMessage> {
|
||||
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<ConversationElement> {
|
||||
val stopwatch = Stopwatch("load($start, $length), thread $threadId")
|
||||
var records: MutableList<MessageRecord> = ArrayList(length)
|
||||
val mentionHelper = MentionHelper()
|
||||
|
@ -146,15 +162,15 @@ class ConversationDataSource(
|
|||
referencedIds.forEach { Recipient.resolved(RecipientId.from(it)) }
|
||||
stopwatch.split("recipient-resolves")
|
||||
|
||||
val messages = records.map { m ->
|
||||
val messages = records.map { record ->
|
||||
ConversationMessageFactory.createWithUnresolvedData(
|
||||
context,
|
||||
m,
|
||||
m.getDisplayBody(context),
|
||||
mentionHelper.getMentions(m.id),
|
||||
quotedHelper.isQuoted(m.id),
|
||||
record,
|
||||
record.getDisplayBody(context),
|
||||
mentionHelper.getMentions(record.id),
|
||||
quotedHelper.isQuoted(record.id),
|
||||
threadRecipient
|
||||
)
|
||||
).toMappingModel()
|
||||
}
|
||||
|
||||
stopwatch.split("conversion")
|
||||
|
@ -163,9 +179,14 @@ class ConversationDataSource(
|
|||
return messages
|
||||
}
|
||||
|
||||
override fun load(messageId: MessageId): ConversationMessage? {
|
||||
val stopwatch = Stopwatch("load($messageId), thread $threadId")
|
||||
var record = SignalDatabase.messages.getMessageRecordOrNull(messageId.id)
|
||||
override fun load(key: ConversationElementKey): ConversationElement? {
|
||||
if (key !is MessageBackedKey) {
|
||||
Log.w(TAG, "Loading non-message related id $key")
|
||||
return null
|
||||
}
|
||||
|
||||
val stopwatch = Stopwatch("load($key), thread $threadId")
|
||||
var record = SignalDatabase.messages.getMessageRecordOrNull(key.id)
|
||||
|
||||
if ((record as? MediaMmsMessageRecord)?.parentStoryId?.isGroupReply() == true) {
|
||||
return null
|
||||
|
@ -182,17 +203,17 @@ class ConversationDataSource(
|
|||
if (record == null) {
|
||||
return null
|
||||
} else {
|
||||
val mentions = SignalDatabase.mentions.getMentionsForMessage(messageId.id)
|
||||
val mentions = SignalDatabase.mentions.getMentionsForMessage(key.id)
|
||||
stopwatch.split("mentions")
|
||||
|
||||
val isQuoted = SignalDatabase.messages.isQuoted(record)
|
||||
stopwatch.split("is-quoted")
|
||||
|
||||
val reactions = SignalDatabase.reactions.getReactions(messageId)
|
||||
val reactions = SignalDatabase.reactions.getReactions(MessageId(key.id))
|
||||
record = ReactionHelper.recordWithReactions(record, reactions)
|
||||
stopwatch.split("reactions")
|
||||
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId.id)
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(key.id)
|
||||
if (attachments.size > 0) {
|
||||
record = (record as MediaMmsMessageRecord).withAttachments(context, attachments)
|
||||
}
|
||||
|
@ -218,14 +239,35 @@ class ConversationDataSource(
|
|||
mentions,
|
||||
isQuoted,
|
||||
threadRecipient
|
||||
)
|
||||
).toMappingModel()
|
||||
}
|
||||
} finally {
|
||||
stopwatch.stop(TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKey(conversationMessage: ConversationMessage): MessageId {
|
||||
return MessageId(conversationMessage.messageRecord.id)
|
||||
override fun getKey(conversationMessage: ConversationElement): ConversationElementKey {
|
||||
return when (conversationMessage) {
|
||||
is ConversationMessageElement -> MessageBackedKey(conversationMessage.conversationMessage.messageRecord.id)
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ConversationMessage.toMappingModel(): MappingModel<*> {
|
||||
return if (messageRecord.isUpdate) {
|
||||
ConversationUpdate(this)
|
||||
} else if (messageRecord.isOutgoing) {
|
||||
if (this.isTextOnly(context)) {
|
||||
OutgoingTextOnly(this)
|
||||
} else {
|
||||
OutgoingMedia(this)
|
||||
}
|
||||
} else {
|
||||
if (this.isTextOnly(context)) {
|
||||
IncomingTextOnly(this)
|
||||
} else {
|
||||
IncomingMedia(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.data
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
|
||||
sealed interface ConversationMessageElement {
|
||||
val conversationMessage: ConversationMessage
|
||||
}
|
||||
|
||||
data class ConversationUpdate(
|
||||
override val conversationMessage: ConversationMessage
|
||||
) : ConversationMessageElement, MappingModel<ConversationUpdate> {
|
||||
override fun areItemsTheSame(newItem: ConversationUpdate): Boolean {
|
||||
return conversationMessage.messageRecord.id == newItem.conversationMessage.messageRecord.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: ConversationUpdate): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data class OutgoingTextOnly(
|
||||
override val conversationMessage: ConversationMessage
|
||||
) : ConversationMessageElement, MappingModel<OutgoingTextOnly> {
|
||||
override fun areItemsTheSame(newItem: OutgoingTextOnly): Boolean {
|
||||
return conversationMessage.messageRecord.id == newItem.conversationMessage.messageRecord.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: OutgoingTextOnly): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data class OutgoingMedia(
|
||||
override val conversationMessage: ConversationMessage
|
||||
) : ConversationMessageElement, MappingModel<OutgoingMedia> {
|
||||
override fun areItemsTheSame(newItem: OutgoingMedia): Boolean {
|
||||
return conversationMessage.messageRecord.id == newItem.conversationMessage.messageRecord.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: OutgoingMedia): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data class IncomingTextOnly(
|
||||
override val conversationMessage: ConversationMessage
|
||||
) : ConversationMessageElement, MappingModel<IncomingTextOnly> {
|
||||
override fun areItemsTheSame(newItem: IncomingTextOnly): Boolean {
|
||||
return conversationMessage.messageRecord.id == newItem.conversationMessage.messageRecord.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: IncomingTextOnly): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
data class IncomingMedia(
|
||||
override val conversationMessage: ConversationMessage
|
||||
) : ConversationMessageElement, MappingModel<IncomingMedia> {
|
||||
override fun areItemsTheSame(newItem: IncomingMedia): Boolean {
|
||||
return conversationMessage.messageRecord.id == newItem.conversationMessage.messageRecord.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: IncomingMedia): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue