Remove most of Conversation Fragment V1 and friends.
This commit is contained in:
parent
9c49c84306
commit
67b8f468e4
39 changed files with 187 additions and 9408 deletions
|
@ -8,6 +8,7 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
|
|
|
@ -630,22 +630,11 @@
|
|||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.BubbleConversationActivity"
|
||||
android:theme="@style/Signal.DayNight"
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
android:theme="@style/Signal.DayNight"
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".conversation.ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
|
|
|
@ -14,8 +14,6 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
|||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.lang.AssertionError
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* The send button you see in a conversation.
|
||||
|
@ -27,7 +25,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
|||
private val TAG = Log.tag(SendButton::class.java)
|
||||
}
|
||||
|
||||
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
|
||||
private var scheduledSendListener: ScheduledSendListener? = null
|
||||
|
||||
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
|
||||
|
@ -43,16 +40,10 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
|||
ViewUtil.mirrorIfRtl(this, getContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
|
||||
*/
|
||||
val isManualSelection: Boolean
|
||||
get() = activeMessageSendType != null
|
||||
|
||||
/**
|
||||
* The actively-selected send type.
|
||||
*/
|
||||
val selectedSendType: MessageSendType
|
||||
private val selectedSendType: MessageSendType
|
||||
get() {
|
||||
activeMessageSendType?.let {
|
||||
return it
|
||||
|
@ -78,65 +69,33 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
|||
if (signalType != null) {
|
||||
Log.w(TAG, "No options of default type, but Signal type is available. Switching. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
onSelectionChanged(signalType, false)
|
||||
onSelectionChanged(signalType)
|
||||
return signalType
|
||||
} else if (availableSendTypes.isEmpty()) {
|
||||
Log.w(TAG, "No send types available at all! Enabling the Signal transport.")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
availableSendTypes = listOf(MessageSendType.SignalMessageSendType)
|
||||
onSelectionChanged(MessageSendType.SignalMessageSendType, false)
|
||||
onSelectionChanged(MessageSendType.SignalMessageSendType)
|
||||
return MessageSendType.SignalMessageSendType
|
||||
} else {
|
||||
throw AssertionError("No options of default type! DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun triggerSelectedChangedEvent() {
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
onSelectionChanged(newType = selectedSendType)
|
||||
}
|
||||
|
||||
fun setScheduledSendListener(listener: ScheduledSendListener?) {
|
||||
this.scheduledSendListener = listener
|
||||
}
|
||||
|
||||
fun resetAvailableTransports(isMediaMessage: Boolean) {
|
||||
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
|
||||
activeMessageSendType = null
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
defaultSubscriptionId = null
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun disableTransportType(type: MessageSendType.TransportType) {
|
||||
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
|
||||
}
|
||||
|
||||
fun setDefaultTransport(type: MessageSendType.TransportType) {
|
||||
if (defaultTransportType == type) {
|
||||
return
|
||||
}
|
||||
defaultTransportType = type
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun setSendType(sendType: MessageSendType?) {
|
||||
private fun setSendType(sendType: MessageSendType?) {
|
||||
if (activeMessageSendType == sendType) {
|
||||
return
|
||||
}
|
||||
activeMessageSendType = sendType
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
|
||||
}
|
||||
|
||||
fun setDefaultSubscriptionId(subscriptionId: Int?) {
|
||||
if (defaultSubscriptionId == subscriptionId) {
|
||||
return
|
||||
}
|
||||
defaultSubscriptionId = subscriptionId
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
onSelectionChanged(newType = selectedSendType)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -146,25 +105,9 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
|||
popupContainer = container
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
|
||||
private fun onSelectionChanged(newType: MessageSendType) {
|
||||
setImageResource(newType.buttonDrawableRes)
|
||||
contentDescription = context.getString(newType.titleRes)
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onSendTypeChanged(newType, isManualSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSendTypeMenu(): Boolean {
|
||||
return if (availableSendTypes.size == 1) {
|
||||
if (scheduledSendListener == null && snackbarContainer != null && !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
Snackbar.make(snackbarContainer!!, R.string.InputPanel__sms_messaging_is_no_longer_supported_in_signal, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
false
|
||||
} else {
|
||||
showSendTypeContextMenu(false)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
|
@ -216,10 +159,6 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
|||
.show(items)
|
||||
}
|
||||
|
||||
fun interface SendTypeChangedListener {
|
||||
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
|
||||
}
|
||||
|
||||
interface ScheduledSendListener {
|
||||
fun onSendScheduled()
|
||||
fun canSchedule(): Boolean
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Activity which encapsulates a conversation for a Bubble window.
|
||||
*
|
||||
* This activity exists so that we can override some of its manifest parameters
|
||||
* without clashing with {@link ConversationActivity} and provide an API-level
|
||||
* independent "is in bubble?" check.
|
||||
*/
|
||||
public class BubbleConversationActivity extends ConversationActivity {
|
||||
@Override
|
||||
public boolean isInBubble() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
ViewUtil.hideKeyboard(this, getComposeText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeToolbar(@NonNull Toolbar toolbar) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Activity which encapsulates a conversation for a Bubble window.
|
||||
*
|
||||
* This activity exists so that we can override some of its manifest parameters
|
||||
* without clashing with [ConversationActivity] and provide an API-level
|
||||
* independent "is in bubble?" check.
|
||||
*/
|
||||
class BubbleConversationActivity : ConversationActivity() {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ViewUtil.hideKeyboard(this, findViewById(R.id.fragment_container))
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.HidingLinearLayout
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.views.Stub
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), ConversationParentFragment.Callback, DonationPaymentComponent {
|
||||
|
||||
companion object {
|
||||
private const val STATE_WATERMARK = "share_data_watermark"
|
||||
}
|
||||
|
||||
private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS)
|
||||
private lateinit var fragment: ConversationParentFragment
|
||||
private var shareDataTimestamp: Long = -1L
|
||||
|
||||
private val dynamicTheme: DynamicTheme = DynamicNoActionBarTheme()
|
||||
override fun onPreCreate() {
|
||||
dynamicTheme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
supportPostponeEnterTransition()
|
||||
transitionDebouncer.publish { supportStartPostponedEnterTransition() }
|
||||
window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L)
|
||||
} else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) {
|
||||
shareDataTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
setContentView(R.layout.conversation_parent_fragment_container)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
replaceFragment(intent!!)
|
||||
} else {
|
||||
fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) as ConversationParentFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
transitionDebouncer.clear()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putLong(STATE_WATERMARK, shareDataTimestamp)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
setIntent(intent)
|
||||
replaceFragment(intent!!)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
private fun replaceFragment(intent: Intent) {
|
||||
fragment = ConversationParentFragment.create(intent)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.disallowAddToBackStack()
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return fragment.dispatchTouchEvent(ev) || super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
override fun getShareDataTimestamp(): Long {
|
||||
return shareDataTimestamp
|
||||
}
|
||||
|
||||
override fun setShareDataTimestamp(timestamp: Long) {
|
||||
shareDataTimestamp = timestamp
|
||||
}
|
||||
|
||||
override fun onInitializeToolbar(toolbar: Toolbar) {
|
||||
toolbar.navigationIcon = AppCompatResources.getDrawable(this, R.drawable.ic_arrow_left_24)
|
||||
toolbar.setNavigationOnClickListener { finish() }
|
||||
}
|
||||
|
||||
fun getRecipient(): Recipient {
|
||||
return fragment.recipient
|
||||
}
|
||||
|
||||
fun getTitleView(): View {
|
||||
return fragment.titleView
|
||||
}
|
||||
|
||||
fun getComposeText(): View {
|
||||
return fragment.composeText
|
||||
}
|
||||
|
||||
fun getQuickAttachmentToggle(): HidingLinearLayout {
|
||||
return fragment.quickAttachmentToggle
|
||||
}
|
||||
|
||||
fun getReminderView(): Stub<ReminderView> {
|
||||
return fragment.reminderView
|
||||
}
|
||||
|
||||
override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
}
|
|
@ -1,256 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedDataSource;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AttachmentHelper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.CallHelper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.MentionHelper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.PaymentHelper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.QuotedHelper;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ReactionHelper;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Core data source for loading an individual conversation.
|
||||
*/
|
||||
public class ConversationDataSource implements PagedDataSource<MessageId, ConversationMessage> {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationDataSource.class);
|
||||
|
||||
private final Context context;
|
||||
private final long threadId;
|
||||
private final MessageRequestData messageRequestData;
|
||||
private final boolean showUniversalExpireTimerUpdate;
|
||||
|
||||
/** Used once for the initial fetch, then cleared. */
|
||||
private int baseSize;
|
||||
|
||||
private final Recipient threadRecipient;
|
||||
|
||||
public ConversationDataSource(
|
||||
@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MessageRequestData messageRequestData,
|
||||
boolean showUniversalExpireTimerUpdate,
|
||||
int baseSize,
|
||||
@NonNull Recipient threadRecipient
|
||||
) {
|
||||
this.context = context;
|
||||
this.threadId = threadId;
|
||||
this.messageRequestData = messageRequestData;
|
||||
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
|
||||
this.baseSize = baseSize;
|
||||
this.threadRecipient = threadRecipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
long startTime = System.currentTimeMillis();
|
||||
int size = getSizeInternal() +
|
||||
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
|
||||
(messageRequestData.isHidden() ? 1 : 0) +
|
||||
(showUniversalExpireTimerUpdate ? 1 : 0);
|
||||
|
||||
Log.d(TAG, "[size(), thread " + threadId + "] " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
private int getSizeInternal() {
|
||||
synchronized (this) {
|
||||
if (baseSize != -1) {
|
||||
int size = baseSize;
|
||||
baseSize = -1;
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
return SignalDatabase.messages().getMessageCountForThread(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<ConversationMessage> load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
|
||||
List<MessageRecord> records = new ArrayList<>(length);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
QuotedHelper quotedHelper = new QuotedHelper();
|
||||
AttachmentHelper attachmentHelper = new AttachmentHelper();
|
||||
ReactionHelper reactionHelper = new ReactionHelper();
|
||||
PaymentHelper paymentHelper = new PaymentHelper();
|
||||
CallHelper callHelper = new CallHelper();
|
||||
Set<ServiceId> referencedIds = new HashSet<>();
|
||||
|
||||
try (MessageTable.Reader reader = MessageTable.mmsReaderFor(SignalDatabase.messages().getConversation(threadId, start, length))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
quotedHelper.add(record);
|
||||
reactionHelper.add(record);
|
||||
attachmentHelper.add(record);
|
||||
paymentHelper.add(record);
|
||||
callHelper.add(record);
|
||||
|
||||
UpdateDescription description = record.getUpdateDisplayBody(context, null);
|
||||
if (description != null) {
|
||||
referencedIds.addAll(description.getMentioned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
|
||||
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
|
||||
}
|
||||
|
||||
if (messageRequestData.isHidden() && (start + length >= totalSize)) {
|
||||
records.add(new InMemoryMessageRecord.RemovedContactHidden(threadId));
|
||||
}
|
||||
|
||||
if (showUniversalExpireTimerUpdate) {
|
||||
records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
|
||||
}
|
||||
|
||||
stopwatch.split("messages");
|
||||
|
||||
mentionHelper.fetchMentions(context);
|
||||
stopwatch.split("mentions");
|
||||
|
||||
quotedHelper.fetchQuotedState();
|
||||
stopwatch.split("is-quoted");
|
||||
|
||||
reactionHelper.fetchReactions();
|
||||
stopwatch.split("reactions");
|
||||
|
||||
records = reactionHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("reaction-models");
|
||||
|
||||
attachmentHelper.fetchAttachments();
|
||||
stopwatch.split("attachments");
|
||||
|
||||
records = attachmentHelper.buildUpdatedModels(context, records);
|
||||
stopwatch.split("attachment-models");
|
||||
|
||||
paymentHelper.fetchPayments();
|
||||
stopwatch.split("payments");
|
||||
|
||||
records = paymentHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("payment-models");
|
||||
|
||||
callHelper.fetchCalls();
|
||||
stopwatch.split("calls");
|
||||
|
||||
records = callHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("call-models");
|
||||
|
||||
for (ServiceId serviceId : referencedIds) {
|
||||
Recipient.resolved(RecipientId.from(serviceId));
|
||||
}
|
||||
stopwatch.split("recipient-resolves");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId()), threadRecipient))
|
||||
.toList();
|
||||
|
||||
stopwatch.split("conversion");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ConversationMessage load(@NonNull MessageId messageId) {
|
||||
Stopwatch stopwatch = new Stopwatch("load(" + messageId + "), thread " + threadId);
|
||||
MessageRecord record = SignalDatabase.messages().getMessageRecordOrNull(messageId.getId());
|
||||
|
||||
if (record instanceof MediaMmsMessageRecord &&
|
||||
((MediaMmsMessageRecord) record).getParentStoryId() != null &&
|
||||
((MediaMmsMessageRecord) record).getParentStoryId().isGroupReply()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) record).getScheduledDate() != -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
stopwatch.split("message");
|
||||
|
||||
try {
|
||||
if (record != null) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageId.getId());
|
||||
stopwatch.split("mentions");
|
||||
|
||||
boolean isQuoted = SignalDatabase.messages().isQuoted(record);
|
||||
stopwatch.split("is-quoted");
|
||||
|
||||
List<ReactionRecord> reactions = SignalDatabase.reactions().getReactions(messageId);
|
||||
record = ReactionHelper.recordWithReactions(record, reactions);
|
||||
stopwatch.split("reactions");
|
||||
|
||||
List<DatabaseAttachment> attachments = SignalDatabase.attachments().getAttachmentsForMessage(messageId.getId());
|
||||
if (attachments.size() > 0) {
|
||||
record = ((MediaMmsMessageRecord) record).withAttachments(context, attachments);
|
||||
}
|
||||
stopwatch.split("attachments");
|
||||
|
||||
if (record.isPaymentNotification()) {
|
||||
record = SignalDatabase.payments().updateMessageWithPayment(record);
|
||||
}
|
||||
stopwatch.split("payments");
|
||||
|
||||
if (record.isCallLog() && !record.isGroupCall()) {
|
||||
CallTable.Call call = SignalDatabase.calls().getCallByMessageId(record.getId());
|
||||
if (call != null && record instanceof MediaMmsMessageRecord) {
|
||||
record = ((MediaMmsMessageRecord) record).withCall(call);
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.split("calls");
|
||||
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
record.getDisplayBody(ApplicationDependencies.getApplication()),
|
||||
mentions,
|
||||
isQuoted,
|
||||
threadRecipient);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
stopwatch.stop(TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MessageId getKey(@NonNull ConversationMessage conversationMessage) {
|
||||
return new MessageId(conversationMessage.getMessageRecord().getId());
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,293 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupManager;
|
||||
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
final class ConversationGroupViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Recipient> liveRecipient;
|
||||
private final LiveData<GroupActiveState> groupActiveState;
|
||||
private final LiveData<ConversationMemberLevel> selfMembershipLevel;
|
||||
private final LiveData<Integer> actionableRequestingMembers;
|
||||
private final LiveData<ReviewState> reviewState;
|
||||
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
|
||||
private final GroupManagementRepository groupManagementRepository;
|
||||
|
||||
private boolean firstTimeInviteFriendsTriggered;
|
||||
|
||||
private ConversationGroupViewModel() {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
this.groupManagementRepository = new GroupManagementRepository();
|
||||
|
||||
LiveData<GroupRecord> groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
|
||||
LiveData<List<Recipient>> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> {
|
||||
if (record != null && record.isV2Group()) {
|
||||
return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2()))
|
||||
.map(ReviewRecipient::getRecipient)
|
||||
.toList();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
});
|
||||
|
||||
this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState));
|
||||
this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel));
|
||||
this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount));
|
||||
this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions));
|
||||
this.reviewState = LiveDataUtil.combineLatest(groupRecord,
|
||||
duplicates,
|
||||
(record, dups) -> dups.isEmpty()
|
||||
? ReviewState.EMPTY
|
||||
: new ReviewState(record.getId().requireV2(), dups.get(0), dups.size()));
|
||||
}
|
||||
|
||||
void onRecipientChange(Recipient recipient) {
|
||||
liveRecipient.setValue(recipient);
|
||||
}
|
||||
|
||||
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
if (groupId.isV2()) {
|
||||
SignalDatabase.groups().removeUnmigratedV1Members(groupId.requireV2());
|
||||
liveRecipient.postValue(liveRecipient.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of pending group join requests that can be actioned by this client.
|
||||
*/
|
||||
LiveData<Integer> getActionableRequestingMembers() {
|
||||
return actionableRequestingMembers;
|
||||
}
|
||||
|
||||
LiveData<GroupActiveState> getGroupActiveState() {
|
||||
return groupActiveState;
|
||||
}
|
||||
|
||||
LiveData<ConversationMemberLevel> getSelfMemberLevel() {
|
||||
return selfMembershipLevel;
|
||||
}
|
||||
|
||||
public LiveData<ReviewState> getReviewState() {
|
||||
return reviewState;
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<RecipientId>> getGroupV1MigrationSuggestions() {
|
||||
return gv1MigrationSuggestions;
|
||||
}
|
||||
|
||||
boolean isNonAdminInAnnouncementGroup() {
|
||||
ConversationMemberLevel level = selfMembershipLevel.getValue();
|
||||
return level != null && level.getMemberLevel() != GroupTable.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup();
|
||||
}
|
||||
|
||||
private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient != null && recipient.isGroup()) {
|
||||
Application context = ApplicationDependencies.getApplication();
|
||||
GroupTable groupDatabase = SignalDatabase.groups();
|
||||
return groupDatabase.getGroup(recipient.getId()).orElse(null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) {
|
||||
if (record != null &&
|
||||
record.isV2Group() &&
|
||||
record.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR)
|
||||
{
|
||||
return record.requireV2GroupProperties()
|
||||
.getDecryptedGroup()
|
||||
.getRequestingMembersCount();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return new GroupActiveState(record.isActive(), record.isV2Group());
|
||||
}
|
||||
|
||||
private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static List<RecipientId> mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) {
|
||||
if (record == null ||
|
||||
!record.isV2Group() ||
|
||||
!record.isActive() ||
|
||||
record.isPendingMember(Recipient.self())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return Stream.of(record.getUnmigratedV1Members())
|
||||
.filterNot(m -> record.getMembers().contains(m))
|
||||
.map(Recipient::resolved)
|
||||
.filter(GroupsV1MigrationUtil::isAutoMigratable)
|
||||
.map(Recipient::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static void onCancelJoinRequest(@NonNull Recipient recipient,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
if (!recipient.isPushV2Group()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
try {
|
||||
GroupManager.cancelJoinRequest(ApplicationDependencies.getApplication(), recipient.getGroupId().get().requireV2());
|
||||
callback.onComplete(null);
|
||||
} catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
|
||||
if (firstTimeInviteFriendsTriggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstTimeInviteFriendsTriggered = true;
|
||||
|
||||
SimpleTask.run(() -> SignalDatabase.groups()
|
||||
.requireGroup(groupId)
|
||||
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
|
||||
justSelf -> {
|
||||
if (justSelf) {
|
||||
inviteFriends(supportFragmentManager, groupId);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
|
||||
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
|
||||
}
|
||||
|
||||
public Single<GroupBlockJoinRequestResult> blockJoinRequests(@NonNull Recipient groupRecipient, @NonNull Recipient recipient) {
|
||||
return groupManagementRepository.blockJoinRequests(groupRecipient.requireGroupId().requireV2(), recipient)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
static final class ReviewState {
|
||||
|
||||
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final Recipient recipient;
|
||||
private final int count;
|
||||
|
||||
ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) {
|
||||
this.groupId = groupId;
|
||||
this.recipient = recipient;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public @Nullable GroupId.V2 getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
static final class GroupActiveState {
|
||||
private final boolean isActive;
|
||||
private final boolean isActiveV2;
|
||||
|
||||
public GroupActiveState(boolean isActive, boolean isV2) {
|
||||
this.isActive = isActive;
|
||||
this.isActiveV2 = isActive && isV2;
|
||||
}
|
||||
|
||||
public boolean isActiveGroup() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
public boolean isActiveV2Group() {
|
||||
return isActiveV2;
|
||||
}
|
||||
}
|
||||
|
||||
static final class ConversationMemberLevel {
|
||||
private final GroupTable.MemberLevel memberLevel;
|
||||
private final boolean isAnnouncementGroup;
|
||||
|
||||
private ConversationMemberLevel(GroupTable.MemberLevel memberLevel, boolean isAnnouncementGroup) {
|
||||
this.memberLevel = memberLevel;
|
||||
this.isAnnouncementGroup = isAnnouncementGroup;
|
||||
}
|
||||
|
||||
public @NonNull GroupTable.MemberLevel getMemberLevel() {
|
||||
return memberLevel;
|
||||
}
|
||||
|
||||
public boolean isAnnouncementGroup() {
|
||||
return isAnnouncementGroup;
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationGroupViewModel());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,14 +16,13 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
|||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.SlideFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -65,7 +64,6 @@ public class ConversationIntents {
|
|||
* @param context Context for Intent creation
|
||||
* @param recipientId The RecipientId to query the thread ID for if the passed one is invalid.
|
||||
* @param threadId The threadId, or -1L
|
||||
*
|
||||
* @return A Single that will return a builder to create the conversation intent.
|
||||
*/
|
||||
@MainThread
|
||||
|
@ -81,11 +79,11 @@ public class ConversationIntents {
|
|||
}
|
||||
|
||||
public static @NonNull Builder createPopUpBuilder(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
|
||||
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId);
|
||||
return new Builder(context, ConversationPopupActivity.class, recipientId, threadId, ConversationScreenType.POPUP);
|
||||
}
|
||||
|
||||
public static @NonNull Intent createBubbleIntent(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
|
||||
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId).build();
|
||||
return new Builder(context, BubbleConversationActivity.class, recipientId, threadId, ConversationScreenType.BUBBLE).build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,20 +93,11 @@ public class ConversationIntents {
|
|||
* @param context Context for Intent creation
|
||||
* @param recipientId The recipientId, only used if the threadId is not valid
|
||||
* @param threadId The threadId, required for CFV2.
|
||||
*
|
||||
* @return A builder that can be used to create a conversation intent.
|
||||
*/
|
||||
public static @NonNull Builder createBuilderSync(@NonNull Context context, @NonNull RecipientId recipientId, long threadId) {
|
||||
return new Builder(context, recipientId, threadId);
|
||||
}
|
||||
|
||||
static boolean isInvalid(@NonNull Bundle arguments) {
|
||||
Uri uri = getIntentData(arguments);
|
||||
if (isBubbleIntentUri(uri)) {
|
||||
return uri.getQueryParameter(EXTRA_RECIPIENT) == null;
|
||||
} else {
|
||||
return !arguments.containsKey(EXTRA_RECIPIENT);
|
||||
}
|
||||
Preconditions.checkArgument(threadId > 0, "threadId is invalid");
|
||||
return new Builder(context, ConversationActivity.class, recipientId, threadId, ConversationScreenType.NORMAL);
|
||||
}
|
||||
|
||||
static @Nullable Uri getIntentData(@NonNull Bundle bundle) {
|
||||
|
@ -119,7 +108,7 @@ public class ConversationIntents {
|
|||
return bundle.getString(INTENT_TYPE);
|
||||
}
|
||||
|
||||
static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
|
||||
public static @NonNull Bundle createParentFragmentArguments(@NonNull Intent intent) {
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
if (intent.getExtras() != null) {
|
||||
|
@ -132,7 +121,7 @@ public class ConversationIntents {
|
|||
return bundle;
|
||||
}
|
||||
|
||||
static boolean isBubbleIntentUri(@Nullable Uri uri) {
|
||||
public static boolean isBubbleIntentUri(@Nullable Uri uri) {
|
||||
return uri != null && Objects.equals(uri.getAuthority(), BUBBLE_AUTHORITY);
|
||||
}
|
||||
|
||||
|
@ -311,6 +300,7 @@ public class ConversationIntents {
|
|||
private final Class<? extends Activity> conversationActivityClass;
|
||||
private final RecipientId recipientId;
|
||||
private final long threadId;
|
||||
private final ConversationScreenType conversationScreenType;
|
||||
|
||||
private String draftText;
|
||||
private List<Media> media;
|
||||
|
@ -324,30 +314,18 @@ public class ConversationIntents {
|
|||
private boolean withSearchOpen;
|
||||
private Badge giftBadge;
|
||||
private long shareDataTimestamp = -1L;
|
||||
private ConversationScreenType conversationScreenType;
|
||||
|
||||
private Builder(@NonNull Context context,
|
||||
@NonNull RecipientId recipientId,
|
||||
long threadId)
|
||||
{
|
||||
this(
|
||||
context,
|
||||
getBaseConversationActivity(),
|
||||
recipientId,
|
||||
threadId
|
||||
);
|
||||
}
|
||||
|
||||
private Builder(@NonNull Context context,
|
||||
@NonNull Class<? extends Activity> conversationActivityClass,
|
||||
@NonNull RecipientId recipientId,
|
||||
long threadId)
|
||||
long threadId,
|
||||
@NonNull ConversationScreenType conversationScreenType)
|
||||
{
|
||||
this.context = context;
|
||||
this.conversationActivityClass = conversationActivityClass;
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = checkThreadId(threadId);
|
||||
this.conversationScreenType = ConversationScreenType.fromActivityClass(conversationActivityClass);
|
||||
this.conversationScreenType = conversationScreenType;
|
||||
}
|
||||
|
||||
public @NonNull Builder withDraftText(@Nullable String draftText) {
|
||||
|
@ -419,7 +397,7 @@ public class ConversationIntents {
|
|||
|
||||
intent.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
if (Objects.equals(conversationActivityClass, BubbleConversationActivity.class)) {
|
||||
if (conversationScreenType.isInBubble()) {
|
||||
intent.setData(new Uri.Builder().authority(BUBBLE_AUTHORITY)
|
||||
.appendQueryParameter(EXTRA_RECIPIENT, recipientId.serialize())
|
||||
.appendQueryParameter(EXTRA_THREAD_ID, String.valueOf(threadId))
|
||||
|
@ -459,13 +437,9 @@ public class ConversationIntents {
|
|||
intent.setType(dataType);
|
||||
}
|
||||
|
||||
if (FeatureFlags.useConversationFragmentV2()) {
|
||||
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
|
||||
Bundle args = ConversationIntents.createParentFragmentArguments(intent);
|
||||
|
||||
return intent.putExtras(args);
|
||||
} else {
|
||||
return intent;
|
||||
}
|
||||
return intent.putExtras(args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -501,31 +475,13 @@ public class ConversationIntents {
|
|||
|
||||
return NORMAL;
|
||||
}
|
||||
|
||||
private static @NonNull ConversationScreenType fromActivityClass(Class<? extends Activity> activityClass) {
|
||||
if (Objects.equals(activityClass, ConversationPopupActivity.class)) {
|
||||
return POPUP;
|
||||
} else if (Objects.equals(activityClass, BubbleConversationActivity.class)) {
|
||||
return BUBBLE;
|
||||
} else {
|
||||
return NORMAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long checkThreadId(long threadId) {
|
||||
if (threadId < 0 && FeatureFlags.useConversationFragmentV2()) {
|
||||
if (threadId < 0) {
|
||||
throw new IllegalArgumentException("ThreadId is a required field in CFV2");
|
||||
} else {
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<? extends Activity> getBaseConversationActivity() {
|
||||
if (FeatureFlags.useConversationFragmentV2()) {
|
||||
return ConversationActivity.class;
|
||||
} else {
|
||||
return org.thoughtcrime.securesms.conversation.ConversationActivity.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,87 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Display;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class ConversationPopupActivity extends ConversationActivity {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationPopupActivity.class);
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
super.onPreCreate();
|
||||
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND,
|
||||
WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
|
||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||
params.alpha = 1.0f;
|
||||
params.dimAmount = 0.1f;
|
||||
params.gravity = Gravity.TOP;
|
||||
getWindow().setAttributes(params);
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
int width = display.getWidth();
|
||||
int height = display.getHeight();
|
||||
|
||||
if (height > width) getWindow().setLayout((int) (width * .85), (int) (height * .5));
|
||||
else getWindow().setLayout((int) (width * .7), (int) (height * .75));
|
||||
|
||||
super.onCreate(bundle, ready);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
getTitleView().setOnClickListener(null);
|
||||
getComposeText().requestFocus();
|
||||
getQuickAttachmentToggle().disable();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (isFinishing()) overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuInflater inflater = this.getMenuInflater();
|
||||
menu.clear();
|
||||
|
||||
inflater.inflate(R.menu.conversation_popup, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeToolbar(Toolbar toolbar) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendComplete(long threadId) {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUpdateReminders() {
|
||||
if (getReminderView().resolved()) {
|
||||
getReminderView().get().setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.WindowManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
|
||||
/**
|
||||
* Flavor of [ConversationActivity] used for quick replies to notifications in pre-API 24 devices.
|
||||
*/
|
||||
class ConversationPopupActivity : ConversationActivity() {
|
||||
override fun onPreCreate() {
|
||||
super.onPreCreate()
|
||||
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onCreate(bundle: Bundle?, ready: Boolean) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND, WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
|
||||
window.attributes = window.attributes.apply {
|
||||
alpha = 1.0f
|
||||
dimAmount = 0.1f
|
||||
gravity = Gravity.TOP
|
||||
}
|
||||
|
||||
val display = windowManager.defaultDisplay
|
||||
val width = display.width
|
||||
val height = display.height
|
||||
|
||||
if (height > width) {
|
||||
window.setLayout((width * .85).toInt(), (height * .5).toInt())
|
||||
} else {
|
||||
window.setLayout((width * .7).toInt(), (height * .75).toInt())
|
||||
}
|
||||
|
||||
super.onCreate(bundle, ready)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (isFinishing) {
|
||||
overridePendingTransition(R.anim.slide_from_top, R.anim.slide_to_top)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,6 @@ import android.widget.FrameLayout;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Barrier;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
@ -33,7 +32,6 @@ import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
|
|||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.glide.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
|
@ -178,14 +176,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
bottomNavigationBarHeight = 0;
|
||||
}
|
||||
|
||||
if (!FeatureFlags.useConversationFragmentV2()) {
|
||||
toolbarShade.setVisibility(VISIBLE);
|
||||
toolbarShade.setAlpha(1f);
|
||||
|
||||
inputShade.setVisibility(VISIBLE);
|
||||
inputShade.setAlpha(1f);
|
||||
}
|
||||
|
||||
Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap();
|
||||
|
||||
conversationItem.setLayoutParams(new LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
|
||||
|
@ -211,8 +201,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
@NonNull ConversationMessage conversationMessage,
|
||||
@NonNull PointF lastSeenDownPoint,
|
||||
boolean isMessageOnLeft) {
|
||||
updateToolbarShade(activity);
|
||||
updateInputShade(activity);
|
||||
updateToolbarShade();
|
||||
updateInputShade();
|
||||
|
||||
contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(conversationMessage));
|
||||
|
||||
|
@ -394,50 +384,18 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar);
|
||||
}
|
||||
|
||||
private void updateToolbarShade(@NonNull Activity activity) {
|
||||
if (FeatureFlags.useConversationFragmentV2()) {
|
||||
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
|
||||
layoutParams.height = 0;
|
||||
toolbarShade.setLayoutParams(layoutParams);
|
||||
return;
|
||||
}
|
||||
|
||||
View toolbar = activity.findViewById(R.id.toolbar);
|
||||
View bannerContainer = activity.findViewById(FeatureFlags.useConversationFragmentV2() ? R.id.conversation_banner
|
||||
: R.id.conversation_banner_container);
|
||||
|
||||
private void updateToolbarShade() {
|
||||
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
|
||||
layoutParams.height = toolbar.getHeight() + bannerContainer.getHeight();
|
||||
layoutParams.height = 0;
|
||||
toolbarShade.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
private void updateInputShade(@NonNull Activity activity) {
|
||||
if (FeatureFlags.useConversationFragmentV2()) {
|
||||
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
|
||||
layoutParams.height = 0;
|
||||
inputShade.setLayoutParams(layoutParams);
|
||||
return;
|
||||
}
|
||||
|
||||
private void updateInputShade() {
|
||||
LayoutParams layoutParams = (LayoutParams) inputShade.getLayoutParams();
|
||||
layoutParams.bottomMargin = bottomNavigationBarHeight;
|
||||
layoutParams.height = getInputPanelHeight(activity);
|
||||
layoutParams.height = 0;
|
||||
inputShade.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
private int getInputPanelHeight(@NonNull Activity activity) {
|
||||
if (FeatureFlags.useConversationFragmentV2()) {
|
||||
View bottomPanel = activity.findViewById(R.id.conversation_input_panel);
|
||||
|
||||
return bottomPanel.getHeight();
|
||||
}
|
||||
|
||||
View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);
|
||||
View emojiDrawer = activity.findViewById(R.id.emoji_drawer);
|
||||
|
||||
return bottomPanel.getHeight() + (emojiDrawer != null && emojiDrawer.getVisibility() == VISIBLE ? emojiDrawer.getHeight() : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the device is in a configuration where the navigation bar doesn't take up
|
||||
* space at the bottom of the screen.
|
||||
|
@ -915,22 +873,6 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
itemYAnim.setDuration(duration);
|
||||
animators.add(itemYAnim);
|
||||
|
||||
if (!FeatureFlags.useConversationFragmentV2()) {
|
||||
ObjectAnimator toolbarShadeAnim = new ObjectAnimator();
|
||||
toolbarShadeAnim.setProperty(View.ALPHA);
|
||||
toolbarShadeAnim.setFloatValues(0f);
|
||||
toolbarShadeAnim.setTarget(toolbarShade);
|
||||
toolbarShadeAnim.setDuration(duration);
|
||||
animators.add(toolbarShadeAnim);
|
||||
|
||||
ObjectAnimator inputShadeAnim = new ObjectAnimator();
|
||||
inputShadeAnim.setProperty(View.ALPHA);
|
||||
inputShadeAnim.setFloatValues(0f);
|
||||
inputShadeAnim.setTarget(inputShade);
|
||||
inputShadeAnim.setDuration(duration);
|
||||
animators.add(inputShadeAnim);
|
||||
}
|
||||
|
||||
if (activity != null) {
|
||||
ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor);
|
||||
statusBarAnim.setDuration(duration);
|
||||
|
|
|
@ -54,17 +54,6 @@ public class ConversationRepository {
|
|||
this.context = ApplicationDependencies.getApplication();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
boolean canShowAsBubble(long threadId) {
|
||||
if (Build.VERSION.SDK_INT >= ConversationUtil.CONVERSATION_SUPPORT_VERSION) {
|
||||
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
|
||||
|
||||
return recipient != null && BubbleUtil.canBubble(context, recipient.getId(), threadId);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull ConversationData getConversationData(long threadId, @NonNull Recipient conversationRecipient, int jumpToPosition) {
|
||||
ThreadTable.ConversationMetadata metadata = SignalDatabase.threads().getConversationMetadata(threadId);
|
||||
|
@ -143,65 +132,6 @@ public class ConversationRepository {
|
|||
});
|
||||
}
|
||||
|
||||
@NonNull Single<Boolean> checkIfMmsIsEnabled() {
|
||||
return Single.fromCallable(() -> Util.isMmsCapable(context)).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches the given recipient id for changes, and gets the security info for the recipient
|
||||
* whenever a change occurs.
|
||||
*
|
||||
* @param recipientId The recipient id we are interested in
|
||||
*
|
||||
* @return The recipient's security info.
|
||||
*/
|
||||
@NonNull Observable<ConversationSecurityInfo> getSecurityInfo(@NonNull RecipientId recipientId) {
|
||||
return Recipient.observable(recipientId)
|
||||
.distinctUntilChanged((lhs, rhs) -> lhs.isPushGroup() == rhs.isPushGroup() && lhs.getRegistered().equals(rhs.getRegistered()))
|
||||
.switchMapSingle(this::getSecurityInfo)
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
private @NonNull Single<ConversationSecurityInfo> getSecurityInfo(@NonNull Recipient recipient) {
|
||||
return Single.fromCallable(() -> {
|
||||
Log.i(TAG, "Resolving registered state...");
|
||||
RecipientTable.RegisteredState registeredState;
|
||||
|
||||
if (recipient.isPushGroup()) {
|
||||
Log.i(TAG, "Push group recipient...");
|
||||
registeredState = RecipientTable.RegisteredState.REGISTERED;
|
||||
} else {
|
||||
Log.i(TAG, "Checking through resolved recipient");
|
||||
registeredState = recipient.getRegistered();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Resolved registered state: " + registeredState);
|
||||
boolean signalEnabled = Recipient.self().isRegistered();
|
||||
|
||||
if (registeredState == RecipientTable.RegisteredState.UNKNOWN) {
|
||||
try {
|
||||
Log.i(TAG, "Refreshing directory for user: " + recipient.getId().serialize());
|
||||
registeredState = ContactDiscovery.refresh(context, recipient, false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
|
||||
boolean hasUnexportedInsecureMessages = threadId != -1 && SignalDatabase.messages().getUnexportedInsecureMessagesCount(threadId) > 0;
|
||||
|
||||
Log.i(TAG, "Returning registered state...");
|
||||
return new ConversationSecurityInfo(recipient.getId(),
|
||||
registeredState == RecipientTable.RegisteredState.REGISTERED && signalEnabled,
|
||||
Util.isDefaultSmsProvider(context),
|
||||
true,
|
||||
hasUnexportedInsecureMessages,
|
||||
SignalStore.misc().isClientDeprecated(),
|
||||
TextSecurePreferences.isUnauthorizedReceived(context));
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
|
||||
return Single.fromCallable(() -> {
|
||||
|
@ -223,42 +153,4 @@ public class ConversationRepository {
|
|||
}).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
Observable<Integer> getUnreadCount(long threadId, long afterTime) {
|
||||
if (threadId <= -1L || afterTime <= 0L) {
|
||||
return Observable.just(0);
|
||||
}
|
||||
|
||||
return Observable.<Integer> create(emitter -> {
|
||||
|
||||
DatabaseObserver.Observer listener = () -> emitter.onNext(SignalDatabase.messages().getIncomingMeaningfulMessageCountSince(threadId, afterTime));
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(threadId, listener);
|
||||
emitter.setCancellable(() -> ApplicationDependencies.getDatabaseObserver().unregisterObserver(listener));
|
||||
|
||||
listener.onChanged();
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void insertSmsExportUpdateEvent(Recipient recipient) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
|
||||
if (threadId == -1 || !Util.isDefaultSmsProvider(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RecipientUtil.isSmsOnly(threadId, recipient) && (!recipient.isMmsGroup() || Util.isDefaultSmsProvider(context))) {
|
||||
SignalDatabase.messages().insertSmsExportMessage(recipient.getId(), threadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setConversationMuted(@NonNull RecipientId recipientId, long until) {
|
||||
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().setMuted(recipientId, until));
|
||||
}
|
||||
|
||||
public void setConversationDistributionType(long threadId, int distributionType) {
|
||||
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.threads().setDistributionType(threadId, distributionType));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class ConversationStickerViewModel extends ViewModel {
|
||||
|
||||
private final StickerSearchRepository repository;
|
||||
private final MutableLiveData<List<StickerRecord>> stickers;
|
||||
private final MutableLiveData<Boolean> stickersAvailable;
|
||||
private final Throttler availabilityThrottler;
|
||||
private final DatabaseObserver.Observer packObserver;
|
||||
|
||||
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.repository = repository;
|
||||
this.stickers = new MutableLiveData<>();
|
||||
this.stickersAvailable = new MutableLiveData<>();
|
||||
this.availabilityThrottler = new Throttler(500);
|
||||
this.packObserver = () -> {
|
||||
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
|
||||
};
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerStickerPackObserver(packObserver);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<StickerRecord>> getStickerResults() {
|
||||
return stickers;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getStickersAvailability() {
|
||||
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
|
||||
return stickersAvailable;
|
||||
}
|
||||
|
||||
void onInputTextUpdated(@NonNull String text) {
|
||||
if (TextUtils.isEmpty(text) || text.length() > EmojiSource.getLatest().getMaxEmojiLength()) {
|
||||
stickers.setValue(Collections.emptyList());
|
||||
} else {
|
||||
repository.searchByEmoji(text, stickers::postValue);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(packObserver);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
|
||||
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationStickerViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,513 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.signal.paging.ObservablePagedData;
|
||||
import org.signal.paging.PagedData;
|
||||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.signal.paging.ProxyPagingController;
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper;
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.Store;
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore;
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
import kotlin.Unit;
|
||||
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationViewModel.class);
|
||||
|
||||
private final Application context;
|
||||
private final MediaRepository mediaRepository;
|
||||
private final ConversationRepository conversationRepository;
|
||||
private final ScheduledMessagesRepository scheduledMessagesRepository;
|
||||
private final MutableLiveData<List<Media>> recentMedia;
|
||||
private final BehaviorSubject<Long> threadId;
|
||||
private final Observable<MessageData> messageData;
|
||||
private final MutableLiveData<Boolean> showScrollButtons;
|
||||
private final MutableLiveData<Boolean> hasUnreadMentions;
|
||||
private final Observable<Boolean> canShowAsBubble;
|
||||
private final ProxyPagingController<MessageId> pagingController;
|
||||
private final DatabaseObserver.Observer conversationObserver;
|
||||
private final DatabaseObserver.MessageObserver messageUpdateObserver;
|
||||
private final DatabaseObserver.MessageObserver messageInsertObserver;
|
||||
private final BehaviorSubject<RecipientId> recipientId;
|
||||
private final Observable<Optional<ChatWallpaper>> wallpaper;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final Observable<ChatColors> chatColors;
|
||||
private final MutableLiveData<Integer> toolbarBottom;
|
||||
private final MutableLiveData<Integer> inlinePlayerHeight;
|
||||
private final LiveData<Integer> conversationTopMargin;
|
||||
private final Store<ThreadAnimationState> threadAnimationStateStore;
|
||||
private final Observer<ThreadAnimationState> threadAnimationStateStoreDriver;
|
||||
private final NotificationProfilesRepository notificationProfilesRepository;
|
||||
private final MutableLiveData<String> searchQuery;
|
||||
private final GroupAuthorNameColorHelper groupAuthorNameColorHelper;
|
||||
private final RxStore<ConversationState> conversationStateStore;
|
||||
private final CompositeDisposable disposables;
|
||||
private final BehaviorSubject<Unit> conversationStateTick;
|
||||
private final PublishProcessor<Long> markReadRequestPublisher;
|
||||
private final Observable<Integer> scheduledMessageCount;
|
||||
|
||||
private ConversationIntents.Args args;
|
||||
private int jumpToPosition;
|
||||
|
||||
private ConversationViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.mediaRepository = new MediaRepository();
|
||||
this.conversationRepository = new ConversationRepository();
|
||||
this.scheduledMessagesRepository = new ScheduledMessagesRepository();
|
||||
this.recentMedia = new MutableLiveData<>();
|
||||
this.showScrollButtons = new MutableLiveData<>(false);
|
||||
this.hasUnreadMentions = new MutableLiveData<>(false);
|
||||
this.events = new SingleLiveEvent<>();
|
||||
this.pagingController = new ProxyPagingController<>();
|
||||
this.conversationObserver = pagingController::onDataInvalidated;
|
||||
this.messageUpdateObserver = pagingController::onDataItemChanged;
|
||||
this.messageInsertObserver = messageId -> pagingController.onDataItemInserted(messageId, 0);
|
||||
this.toolbarBottom = new MutableLiveData<>();
|
||||
this.inlinePlayerHeight = new MutableLiveData<>();
|
||||
this.conversationTopMargin = Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(toolbarBottom, inlinePlayerHeight, Integer::sum));
|
||||
this.threadAnimationStateStore = new Store<>(new ThreadAnimationState(-1L, null, false));
|
||||
this.notificationProfilesRepository = new NotificationProfilesRepository();
|
||||
this.searchQuery = new MutableLiveData<>();
|
||||
this.recipientId = BehaviorSubject.create();
|
||||
this.threadId = BehaviorSubject.create();
|
||||
this.groupAuthorNameColorHelper = new GroupAuthorNameColorHelper();
|
||||
this.conversationStateStore = new RxStore<>(ConversationState.create(), Schedulers.computation());
|
||||
this.disposables = new CompositeDisposable();
|
||||
this.conversationStateTick = BehaviorSubject.createDefault(Unit.INSTANCE);
|
||||
this.markReadRequestPublisher = PublishProcessor.create();
|
||||
|
||||
BehaviorSubject<Recipient> recipientCache = BehaviorSubject.create();
|
||||
|
||||
recipientId
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map(Recipient::resolved)
|
||||
.subscribe(recipientCache);
|
||||
|
||||
Disposable disposable = conversationStateStore.update(Observable.combineLatest(recipientId.distinctUntilChanged(), conversationStateTick, (id, tick) -> id)
|
||||
.switchMap(conversationRepository::getSecurityInfo)
|
||||
.toFlowable(BackpressureStrategy.LATEST),
|
||||
(securityInfo, state) -> state.withSecurityInfo(securityInfo));
|
||||
|
||||
disposables.add(disposable);
|
||||
|
||||
BehaviorSubject<ConversationData> conversationMetadata = BehaviorSubject.create();
|
||||
|
||||
Observable.combineLatest(threadId, recipientCache, Pair::new)
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map(threadIdAndRecipient -> {
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoadStarted();
|
||||
ConversationData conversationData = conversationRepository.getConversationData(threadIdAndRecipient.first(), threadIdAndRecipient.second(), jumpToPosition);
|
||||
SignalLocalMetrics.ConversationOpen.onMetadataLoaded();
|
||||
|
||||
jumpToPosition = -1;
|
||||
|
||||
return conversationData;
|
||||
})
|
||||
.subscribe(conversationMetadata);
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageUpdateObserver);
|
||||
|
||||
messageData = conversationMetadata
|
||||
.observeOn(Schedulers.io())
|
||||
.switchMap(data -> {
|
||||
int startPosition;
|
||||
|
||||
ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData();
|
||||
|
||||
if (data.shouldJumpToMessage()) {
|
||||
startPosition = data.getJumpToPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) {
|
||||
startPosition = data.getLastSeenPosition();
|
||||
} else if (messageRequestData.isMessageRequestAccepted()) {
|
||||
startPosition = data.getLastScrolledPosition();
|
||||
} else {
|
||||
startPosition = data.getThreadSize();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().registerMessageInsertObserver(data.getThreadId(), messageInsertObserver);
|
||||
|
||||
ConversationDataSource dataSource = new ConversationDataSource(context,
|
||||
data.getThreadId(),
|
||||
messageRequestData,
|
||||
data.showUniversalExpireTimerMessage(),
|
||||
data.getThreadSize(),
|
||||
data.getThreadRecipient());
|
||||
|
||||
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
|
||||
.setBufferPages(2)
|
||||
.setStartIndex(Math.max(startPosition, 0))
|
||||
.build();
|
||||
|
||||
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
|
||||
ObservablePagedData<MessageId, ConversationMessage> pagedData = PagedData.createForObservable(dataSource, config);
|
||||
|
||||
pagingController.set(pagedData.getController());
|
||||
return pagedData.getData();
|
||||
})
|
||||
.observeOn(Schedulers.io())
|
||||
.withLatestFrom(conversationMetadata, (messages, metadata) -> new MessageData(metadata, messages))
|
||||
.doOnNext(a -> SignalLocalMetrics.ConversationOpen.onDataLoaded());
|
||||
|
||||
scheduledMessageCount = threadId
|
||||
.observeOn(Schedulers.io())
|
||||
.switchMap(scheduledMessagesRepository::getScheduledMessageCount);
|
||||
|
||||
Observable<Recipient> liveRecipient = recipientId.distinctUntilChanged().switchMap(id -> Recipient.live(id).observable());
|
||||
|
||||
canShowAsBubble = threadId.observeOn(Schedulers.io()).map(conversationRepository::canShowAsBubble);
|
||||
wallpaper = liveRecipient.map(r -> Optional.ofNullable(r.getWallpaper())).distinctUntilChanged();
|
||||
chatColors = liveRecipient.map(Recipient::getChatColors).distinctUntilChanged();
|
||||
|
||||
threadAnimationStateStore.update(threadId, (id, state) -> {
|
||||
if (state.getThreadId() == id) {
|
||||
return state;
|
||||
} else {
|
||||
return new ThreadAnimationState(id, null, false);
|
||||
}
|
||||
});
|
||||
|
||||
threadAnimationStateStore.update(conversationMetadata, (m, state) -> {
|
||||
if (state.getThreadId() == m.getThreadId()) {
|
||||
return state.copy(state.getThreadId(), m, state.getHasCommittedNonEmptyMessageList());
|
||||
} else {
|
||||
return state.copy(m.getThreadId(), m, false);
|
||||
}
|
||||
});
|
||||
|
||||
this.threadAnimationStateStoreDriver = state -> {};
|
||||
threadAnimationStateStore.getStateLiveData().observeForever(threadAnimationStateStoreDriver);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
Observable<StoryViewState> getStoryViewState() {
|
||||
return recipientId
|
||||
.subscribeOn(Schedulers.io())
|
||||
.switchMap(StoryViewState::getForRecipientId)
|
||||
.distinctUntilChanged()
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void onMessagesCommitted(@NonNull List<ConversationMessage> conversationMessages) {
|
||||
if (Util.hasItems(conversationMessages)) {
|
||||
threadAnimationStateStore.update(state -> {
|
||||
long threadId = conversationMessages.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.map(c -> c.getMessageRecord().getThreadId())
|
||||
.orElse(-2L);
|
||||
|
||||
if (state.getThreadId() == threadId) {
|
||||
return state.copy(state.getThreadId(), state.getThreadMetadata(), true);
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void setDistributionType(int distributionType) {
|
||||
Long threadId = this.threadId.getValue();
|
||||
if (threadId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
conversationRepository.setConversationDistributionType(threadId, distributionType);
|
||||
}
|
||||
|
||||
void submitMarkReadRequest(long timestampSince) {
|
||||
markReadRequestPublisher.onNext(timestampSince);
|
||||
}
|
||||
|
||||
boolean shouldPlayMessageAnimations() {
|
||||
return threadAnimationStateStore.getState().shouldPlayMessageAnimations();
|
||||
}
|
||||
|
||||
void setToolbarBottom(int bottom) {
|
||||
toolbarBottom.setValue(bottom);
|
||||
}
|
||||
|
||||
void setInlinePlayerVisible(boolean isVisible) {
|
||||
inlinePlayerHeight.setValue(isVisible ? ViewUtil.dpToPx(36) : 0);
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
void onConversationDataAvailable(@NonNull RecipientId recipientId, long threadId, int startingPosition) {
|
||||
Log.d(TAG, "[onConversationDataAvailable] recipientId: " + recipientId + ", threadId: " + threadId + ", startingPosition: " + startingPosition);
|
||||
this.jumpToPosition = startingPosition;
|
||||
|
||||
this.threadId.onNext(threadId);
|
||||
this.recipientId.onNext(recipientId);
|
||||
}
|
||||
|
||||
void clearThreadId() {
|
||||
this.jumpToPosition = -1;
|
||||
this.threadId.onNext(-1L);
|
||||
}
|
||||
|
||||
void setSearchQuery(@Nullable String query) {
|
||||
searchQuery.setValue(query);
|
||||
}
|
||||
|
||||
void markGiftBadgeRevealed(long messageId) {
|
||||
conversationRepository.markGiftBadgeRevealed(messageId);
|
||||
}
|
||||
|
||||
void checkIfMmsIsEnabled() {
|
||||
disposables.add(conversationRepository.checkIfMmsIsEnabled().subscribe(isEnabled -> {
|
||||
conversationStateStore.update(state -> state.withMmsEnabled(true));
|
||||
}));
|
||||
}
|
||||
|
||||
@NonNull Flowable<Long> getMarkReadRequests() {
|
||||
return markReadRequestPublisher.onBackpressureBuffer();
|
||||
}
|
||||
|
||||
@NonNull Observable<Integer> getThreadUnreadCount(long afterTime) {
|
||||
return threadId.switchMap(id -> conversationRepository.getUnreadCount(id, afterTime));
|
||||
}
|
||||
|
||||
@NonNull Flowable<ConversationState> getConversationState() {
|
||||
return conversationStateStore.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull Flowable<ConversationSecurityInfo> getConversationSecurityInfo(@NonNull RecipientId recipientId) {
|
||||
return getConversationState().map(ConversationState::getSecurityInfo)
|
||||
.filter(info -> info.isInitialized() && Objects.equals(info.getRecipientId(), recipientId))
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
void updateSecurityInfo() {
|
||||
conversationStateTick.onNext(Unit.INSTANCE);
|
||||
}
|
||||
|
||||
boolean isDefaultSmsApplication() {
|
||||
return conversationStateStore.getState().getSecurityInfo().isDefaultSmsApplication();
|
||||
}
|
||||
|
||||
boolean isPushAvailable() {
|
||||
return conversationStateStore.getState().getSecurityInfo().isPushAvailable();
|
||||
}
|
||||
|
||||
void muteConversation(long until) {
|
||||
conversationRepository.setConversationMuted(args.getRecipientId(), until);
|
||||
}
|
||||
|
||||
@NonNull ConversationState getConversationStateSnapshot() {
|
||||
return conversationStateStore.getState();
|
||||
}
|
||||
|
||||
@NonNull LiveData<String> getSearchQuery() {
|
||||
return searchQuery;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Integer> getConversationTopMargin() {
|
||||
return conversationTopMargin;
|
||||
}
|
||||
|
||||
@NonNull Observable<Boolean> canShowAsBubble() {
|
||||
return canShowAsBubble
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowScrollToBottom() {
|
||||
return Transformations.distinctUntilChanged(showScrollButtons);
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getShowMentionsButton() {
|
||||
return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
|
||||
}
|
||||
|
||||
@NonNull Observable<Optional<ChatWallpaper>> getWallpaper() {
|
||||
return wallpaper
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvents() {
|
||||
return events;
|
||||
}
|
||||
|
||||
@NonNull Observable<ChatColors> getChatColors() {
|
||||
return chatColors
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull Observable<Integer> getScheduledMessageCount() {
|
||||
return scheduledMessageCount.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void setHasUnreadMentions(boolean hasUnreadMentions) {
|
||||
this.hasUnreadMentions.setValue(hasUnreadMentions);
|
||||
}
|
||||
|
||||
boolean getShowScrollButtons() {
|
||||
return this.showScrollButtons.getValue();
|
||||
}
|
||||
|
||||
void setShowScrollButtons(boolean showScrollButtons) {
|
||||
this.showScrollButtons.setValue(showScrollButtons);
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<Media>> getRecentMedia() {
|
||||
return recentMedia;
|
||||
}
|
||||
|
||||
@NonNull Observable<MessageData> getMessageData() {
|
||||
return messageData
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull PagingController<MessageId> getPagingController() {
|
||||
return pagingController;
|
||||
}
|
||||
|
||||
@NonNull Observable<Map<RecipientId, NameColor>> getNameColorsMap() {
|
||||
return recipientId
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged()
|
||||
.map(Recipient::resolved)
|
||||
.map(recipient -> {
|
||||
if (recipient.getGroupId().isPresent()) {
|
||||
return groupAuthorNameColorHelper.getColorMap(recipient.getGroupId().get());
|
||||
} else {
|
||||
return Collections.<RecipientId, NameColor>emptyMap();
|
||||
}
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<NotificationProfile>> getActiveNotificationProfile() {
|
||||
Flowable<Optional<NotificationProfile>> activeProfile = notificationProfilesRepository.getProfiles()
|
||||
.map(profiles -> Optional.ofNullable(NotificationProfiles.getActiveProfile(profiles)));
|
||||
|
||||
return LiveDataReactiveStreams.fromPublisher(activeProfile);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
|
||||
return conversationRepository.resolveMessageToEdit(message);
|
||||
}
|
||||
|
||||
void setArgs(@NonNull ConversationIntents.Args args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@NonNull ConversationIntents.Args getArgs() {
|
||||
return Objects.requireNonNull(args);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.POSTING)
|
||||
public void onRecaptchaRequiredEvent(@NonNull RecaptchaRequiredEvent event) {
|
||||
events.postValue(Event.SHOW_RECAPTCHA);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver);
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver);
|
||||
disposables.clear();
|
||||
conversationStateStore.dispose();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
public void insertSmsExportUpdateEvent(@NonNull Recipient recipient) {
|
||||
conversationRepository.insertSmsExportUpdateEvent(recipient);
|
||||
}
|
||||
|
||||
enum Event {
|
||||
SHOW_RECAPTCHA
|
||||
}
|
||||
|
||||
static class MessageData {
|
||||
private final List<ConversationMessage> messages;
|
||||
private final ConversationData metadata;
|
||||
|
||||
MessageData(@NonNull ConversationData metadata, @NonNull List<ConversationMessage> messages) {
|
||||
this.metadata = metadata;
|
||||
this.messages = messages;
|
||||
}
|
||||
|
||||
public @NonNull List<ConversationMessage> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public @NonNull ConversationData getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ConversationViewModel());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
|
||||
class LastSeenHeader extends StickyHeaderDecoration {
|
||||
|
||||
private final ConversationAdapter adapter;
|
||||
private final long lastSeenTimestamp;
|
||||
|
||||
private long unreadCount;
|
||||
|
||||
LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) {
|
||||
super(adapter, false, false, ConversationAdapter.HEADER_TYPE_LAST_SEEN);
|
||||
this.adapter = adapter;
|
||||
this.lastSeenTimestamp = lastSeenTimestamp;
|
||||
}
|
||||
|
||||
public void setUnreadCount(long unreadCount) {
|
||||
this.unreadCount = unreadCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
if (lastSeenTimestamp <= 0 || unreadCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long currentRecordTimestamp = adapter.getReceivedTimestamp(position);
|
||||
long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1);
|
||||
|
||||
return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) {
|
||||
return parent.getLayoutManager().getDecoratedTop(child);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull RecyclerView.ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) {
|
||||
StickyHeaderViewHolder viewHolder = new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_last_seen, parent, false));
|
||||
adapter.onBindLastSeenViewHolder(viewHolder, unreadCount);
|
||||
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width);
|
||||
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height);
|
||||
|
||||
viewHolder.itemView.measure(childWidth, childHeight);
|
||||
viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight());
|
||||
|
||||
return viewHolder;
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class MessageCountsViewModel extends ViewModel {
|
||||
|
||||
private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED);
|
||||
|
||||
private final Application context;
|
||||
private final MutableLiveData<Long> threadId = new MutableLiveData<>(-1L);
|
||||
private final LiveData<Pair<Integer, Integer>> unreadCounts;
|
||||
|
||||
private DatabaseObserver.Observer observer;
|
||||
|
||||
public MessageCountsViewModel() {
|
||||
this.context = ApplicationDependencies.getApplication();
|
||||
this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> {
|
||||
|
||||
MutableLiveData<Pair<Integer, Integer>> counts = new MutableLiveData<>(new Pair<>(0, 0));
|
||||
|
||||
if (id == -1L) {
|
||||
return counts;
|
||||
}
|
||||
|
||||
if (observer != null) {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
||||
}
|
||||
|
||||
observer = new DatabaseObserver.Observer() {
|
||||
private int previousUnreadCount = -1;
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
EXECUTOR.execute(() -> {
|
||||
int unreadCount = getUnreadCount(context, id);
|
||||
if (unreadCount != previousUnreadCount) {
|
||||
previousUnreadCount = unreadCount;
|
||||
counts.postValue(new Pair<>(unreadCount, getUnreadMentionsCount(context, id)));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
observer.onChanged();
|
||||
|
||||
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
|
||||
|
||||
return counts;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void setThreadId(long threadId) {
|
||||
this.threadId.setValue(threadId);
|
||||
}
|
||||
|
||||
void clearThreadId() {
|
||||
this.threadId.postValue(-1L);
|
||||
}
|
||||
|
||||
@NonNull LiveData<Integer> getUnreadMessagesCount() {
|
||||
return Transformations.map(unreadCounts, Pair::first);
|
||||
}
|
||||
|
||||
@NonNull LiveData<Integer> getUnreadMentionsCount() {
|
||||
return Transformations.map(unreadCounts, Pair::second);
|
||||
}
|
||||
|
||||
private int getUnreadCount(@NonNull Context context, long threadId) {
|
||||
ThreadRecord threadRecord = SignalDatabase.threads().getThreadRecord(threadId);
|
||||
return threadRecord != null ? threadRecord.getUnreadCount() : 0;
|
||||
}
|
||||
|
||||
private int getUnreadMentionsCount(@NonNull Context context, long threadId) {
|
||||
return SignalDatabase.messages().getUnreadMentionCount(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (observer != null) {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import android.net.Uri
|
|||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.concurrent.MaybeCompat
|
||||
|
@ -167,22 +166,16 @@ class DraftRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) {
|
||||
require(threadId != -1L || recipient != null)
|
||||
fun saveDrafts(threadId: Long, drafts: Drafts) {
|
||||
require(threadId != -1L)
|
||||
|
||||
saveDraftsExecutor.execute {
|
||||
if (drafts.isNotEmpty()) {
|
||||
val actualThreadId = if (threadId == -1L) {
|
||||
threadTable.getOrCreateThreadIdFor(recipient!!, distributionType)
|
||||
} else {
|
||||
threadId
|
||||
}
|
||||
|
||||
draftTable.replaceDrafts(actualThreadId, drafts)
|
||||
draftTable.replaceDrafts(threadId, drafts)
|
||||
if (drafts.shouldUpdateSnippet()) {
|
||||
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
|
||||
threadTable.updateSnippet(threadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
|
||||
} else {
|
||||
threadTable.update(actualThreadId, unarchive = false, allowDeletion = false)
|
||||
threadTable.update(threadId, unarchive = false, allowDeletion = false)
|
||||
}
|
||||
} else if (threadId > 0) {
|
||||
draftTable.clearDrafts(threadId)
|
||||
|
@ -191,13 +184,6 @@ class DraftRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
|
||||
return Single.fromCallable {
|
||||
loadDraftsInternal(threadId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun loadDraftsInternal(threadId: Long): DatabaseDraft {
|
||||
val drafts: Drafts = draftTable.getDrafts(threadId)
|
||||
val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
|
||||
|
@ -218,11 +204,6 @@ class DraftRepository(
|
|||
return DatabaseDraft(drafts, updatedText)
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
|
||||
return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) }
|
||||
}
|
||||
|
||||
private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? {
|
||||
val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let {
|
||||
|
@ -237,11 +218,6 @@ class DraftRepository(
|
|||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient)
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
|
||||
return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) }
|
||||
}
|
||||
|
||||
private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? {
|
||||
val messageId = MessageId.deserialize(serialized)
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.drafts
|
|||
|
||||
import org.thoughtcrime.securesms.database.DraftTable
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Drafts
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* State object responsible for holding Voice Note draft state. The intention is to allow
|
||||
|
@ -10,11 +9,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
|||
* management pattern going forward for drafts.
|
||||
*/
|
||||
data class DraftState(
|
||||
@Deprecated("Not needed for CFv2")
|
||||
val recipientId: RecipientId? = null,
|
||||
val threadId: Long = -1,
|
||||
@Deprecated("Not needed for CFv2")
|
||||
val distributionType: Int = 0,
|
||||
val textDraft: DraftTable.Draft? = null,
|
||||
val bodyRangesDraft: DraftTable.Draft? = null,
|
||||
val quoteDraft: DraftTable.Draft? = null,
|
||||
|
@ -24,7 +19,7 @@ data class DraftState(
|
|||
) {
|
||||
|
||||
fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState {
|
||||
return DraftState(recipientId = recipientId, threadId = threadId, distributionType = distributionType)
|
||||
return DraftState(threadId = threadId)
|
||||
}
|
||||
|
||||
fun toDrafts(): Drafts {
|
||||
|
|
|
@ -4,17 +4,13 @@ import androidx.lifecycle.ViewModel
|
|||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Draft
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.QuoteId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
@ -40,16 +36,6 @@ class DraftViewModel @JvmOverloads constructor(
|
|||
store.dispose()
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun setThreadId(threadId: Long) {
|
||||
store.update { it.copy(threadId = threadId) }
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun setDistributionType(distributionType: Int) {
|
||||
store.update { it.copy(distributionType = distributionType) }
|
||||
}
|
||||
|
||||
fun saveEphemeralVoiceNoteDraft(draft: Draft) {
|
||||
store.update { draftState ->
|
||||
saveDrafts(draftState.copy(voiceNoteDraft = draft))
|
||||
|
@ -67,11 +53,6 @@ class DraftViewModel @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun onRecipientChanged(recipient: Recipient) {
|
||||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
|
||||
fun setMessageEditDraft(messageId: MessageId, text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
|
||||
store.update {
|
||||
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
|
||||
|
@ -140,34 +121,10 @@ class DraftViewModel @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun saveDrafts(state: DraftState): DraftState {
|
||||
repository.saveDrafts(state.recipientId?.let { Recipient.resolved(it) }, state.threadId, state.distributionType, state.toDrafts())
|
||||
repository.saveDrafts(state.threadId, state.toDrafts())
|
||||
return state
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDrafts(threadId: Long): Single<DraftRepository.DatabaseDraft> {
|
||||
return repository
|
||||
.loadDrafts(threadId)
|
||||
.doOnSuccess { drafts ->
|
||||
store.update { saveDrafts(it.copyAndSetDrafts(threadId, drafts.drafts)) }
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftQuote(serialized: String): Maybe<ConversationMessage> {
|
||||
return repository.loadDraftQuote(serialized)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@Deprecated("Not needed for CFv2")
|
||||
fun loadDraftEditMessage(serialized: String): Maybe<ConversationMessage> {
|
||||
return repository.loadDraftMessageEdit(serialized)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun loadShareOrDraftData(lastShareDataTimestamp: Long): Maybe<DraftRepository.ShareOrDraftData> {
|
||||
return repository.getShareOrDraftData(lastShareDataTimestamp)
|
||||
.doOnSuccess { (_, drafts) ->
|
||||
|
|
|
@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge
|
|||
import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
|
@ -402,12 +401,6 @@ class MultiselectItemDecoration(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!FeatureFlags.useConversationFragmentV2()) {
|
||||
canvas.clipPath(path)
|
||||
canvas.drawShade()
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -422,12 +415,6 @@ class MultiselectItemDecoration(
|
|||
path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW)
|
||||
}
|
||||
}
|
||||
|
||||
if (!FeatureFlags.useConversationFragmentV2()) {
|
||||
canvas.clipPath(path, Region.Op.DIFFERENCE)
|
||||
canvas.drawShade()
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.DeviceProperties;
|
||||
|
@ -123,8 +122,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
|
|||
@Override
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
if (getParentFragment() instanceof ConversationFragment) {
|
||||
((ConversationFragment) getParentFragment()).refreshList();
|
||||
if (getParentFragment() instanceof Callback) {
|
||||
((Callback) getParentFragment()).onCallNotificationSettingsDialogDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,4 +229,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment {
|
|||
|
||||
return bitmask;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onCallNotificationSettingsDialogDismissed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.groupcall;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class GroupCallViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = Log.tag(GroupCallViewModel.class);
|
||||
|
||||
private final MutableLiveData<Boolean> activeGroup;
|
||||
private final MutableLiveData<Boolean> ongoingGroupCall;
|
||||
private final LiveData<Boolean> activeGroupCall;
|
||||
private final MutableLiveData<Boolean> groupCallHasCapacity;
|
||||
|
||||
private @Nullable Recipient currentRecipient;
|
||||
|
||||
GroupCallViewModel() {
|
||||
this.activeGroup = new MutableLiveData<>(false);
|
||||
this.ongoingGroupCall = new MutableLiveData<>(false);
|
||||
this.groupCallHasCapacity = new MutableLiveData<>(false);
|
||||
this.activeGroupCall = LiveDataUtil.combineLatest(activeGroup, ongoingGroupCall, (active, ongoing) -> active && ongoing);
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Boolean> hasActiveGroupCall() {
|
||||
return activeGroupCall;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<Boolean> groupCallHasCapacity() {
|
||||
return groupCallHasCapacity;
|
||||
}
|
||||
|
||||
public void onRecipientChange(@Nullable Recipient recipient) {
|
||||
activeGroup.postValue(recipient != null && recipient.isActiveGroup());
|
||||
|
||||
if (Objects.equals(currentRecipient, recipient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ongoingGroupCall.postValue(false);
|
||||
groupCallHasCapacity.postValue(false);
|
||||
|
||||
currentRecipient = recipient;
|
||||
|
||||
peekGroupCall();
|
||||
}
|
||||
|
||||
public void peekGroupCall() {
|
||||
if (isGroupCallCapable(currentRecipient)) {
|
||||
Log.i(TAG, "peek call for " + currentRecipient.getId());
|
||||
ApplicationDependencies.getSignalCallManager().peekGroupCall(currentRecipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) {
|
||||
if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) {
|
||||
Log.i(TAG, "update UI with call event: ongoing call: " + groupCallPeekEvent.isOngoing() + " hasCapacity: " + groupCallPeekEvent.callHasCapacity());
|
||||
|
||||
ongoingGroupCall.postValue(groupCallPeekEvent.isOngoing());
|
||||
groupCallHasCapacity.postValue(groupCallPeekEvent.callHasCapacity());
|
||||
} else {
|
||||
Log.i(TAG, "Ignore call event for different recipient.");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isGroupCallCapable(@Nullable Recipient recipient) {
|
||||
return recipient != null && recipient.isActiveGroup() && recipient.isPushV2Group() && Build.VERSION.SDK_INT > 19;
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new GroupCallViewModel());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
@ -20,7 +21,7 @@ import java.util.concurrent.TimeUnit
|
|||
/**
|
||||
* Wrapper activity for ConversationFragment.
|
||||
*/
|
||||
class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent {
|
||||
|
||||
companion object {
|
||||
private const val STATE_WATERMARK = "share_data_watermark"
|
||||
|
@ -88,7 +89,11 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl
|
|||
|
||||
private fun replaceFragment() {
|
||||
val fragment = ConversationFragment().apply {
|
||||
arguments = intent.extras
|
||||
arguments = if (ConversationIntents.isBubbleIntentUri(intent.data)) {
|
||||
ConversationIntents.createParentFragmentArguments(intent)
|
||||
} else {
|
||||
intent.extras
|
||||
}
|
||||
}
|
||||
|
||||
supportFragmentManager
|
||||
|
|
|
@ -120,6 +120,7 @@ import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
|
@ -331,7 +332,8 @@ class ConversationFragment :
|
|||
ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
|
||||
ScheduleMessageDialogCallback,
|
||||
ConversationBottomSheetCallback,
|
||||
SafetyNumberBottomSheet.Callbacks {
|
||||
SafetyNumberBottomSheet.Callbacks,
|
||||
EnableCallNotificationSettingsDialog.Callback {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationFragment::class.java)
|
||||
|
@ -458,7 +460,6 @@ class ConversationFragment :
|
|||
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
|
||||
private lateinit var conversationItemDecorations: ConversationItemDecorations
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
private lateinit var menuProvider: ConversationOptionsMenu.Provider
|
||||
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
|
||||
private lateinit var backPressedCallback: BackPressedDelegate
|
||||
|
||||
|
@ -471,6 +472,7 @@ class ConversationFragment :
|
|||
private var reShowScheduleMessagesBar: Boolean = false
|
||||
private var composeTextEventsListener: ComposeTextEventsListener? = null
|
||||
private var dataObserver: DataObserver? = null
|
||||
private var menuProvider: ConversationOptionsMenu.Provider? = null
|
||||
|
||||
private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy {
|
||||
override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) {
|
||||
|
@ -559,6 +561,8 @@ class ConversationFragment :
|
|||
|
||||
binding.conversationVideoContainer.setClipToOutline(true)
|
||||
|
||||
SpoilerAnnotation.resetRevealedSpoilers()
|
||||
|
||||
registerForResults()
|
||||
}
|
||||
|
||||
|
@ -761,6 +765,10 @@ class ConversationFragment :
|
|||
|
||||
override fun onCanceled() = Unit
|
||||
|
||||
override fun onCallNotificationSettingsDialogDismissed() {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
private fun observeConversationThread() {
|
||||
|
@ -813,7 +821,7 @@ class ConversationFragment :
|
|||
backPressedCallback = BackPressedDelegate()
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback)
|
||||
|
||||
menuProvider.afterFirstRenderMode = true
|
||||
menuProvider?.afterFirstRenderMode = true
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(LastScrolledPositionUpdater(adapter, layoutManager, viewModel))
|
||||
|
||||
|
@ -858,7 +866,7 @@ class ConversationFragment :
|
|||
setOnClickListener(sendButtonListener)
|
||||
setScheduledSendListener(sendButtonListener)
|
||||
isEnabled = true
|
||||
post { sendButton.triggerSelectedChangedEvent() }
|
||||
sendButton.triggerSelectedChangedEvent()
|
||||
}
|
||||
|
||||
sendEditButton.setOnClickListener { handleSendEditMessage() }
|
||||
|
@ -981,6 +989,11 @@ class ConversationFragment :
|
|||
|
||||
val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() }
|
||||
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
|
||||
|
||||
if (args.conversationScreenType.isInPopup) {
|
||||
composeText.requestFocus()
|
||||
binding.conversationInputPanel.quickAttachmentToggle.disable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeInlineSearch() {
|
||||
|
@ -1162,15 +1175,17 @@ class ConversationFragment :
|
|||
}
|
||||
|
||||
private fun presentActionBarMenu() {
|
||||
optionsMenuCallback = ConversationOptionsMenuCallback()
|
||||
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
|
||||
binding.toolbar.addMenuProvider(menuProvider)
|
||||
invalidateOptionsMenu()
|
||||
if (!args.conversationScreenType.isInPopup) {
|
||||
optionsMenuCallback = ConversationOptionsMenuCallback()
|
||||
menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables)
|
||||
binding.toolbar.addMenuProvider(menuProvider!!)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
when (args.conversationScreenType) {
|
||||
ConversationScreenType.NORMAL -> presentNavigationIconForNormal()
|
||||
ConversationScreenType.BUBBLE -> presentNavigationIconForBubble()
|
||||
ConversationScreenType.POPUP -> Unit
|
||||
ConversationScreenType.BUBBLE,
|
||||
ConversationScreenType.POPUP -> presentNavigationIconForBubble()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1209,8 +1224,10 @@ class ConversationFragment :
|
|||
titleView.clearExpiring()
|
||||
}
|
||||
|
||||
titleView.setOnClickListener {
|
||||
optionsMenuCallback.handleConversationSettings()
|
||||
if (!args.conversationScreenType.isInPopup) {
|
||||
titleView.setOnClickListener {
|
||||
optionsMenuCallback.handleConversationSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1751,7 +1768,9 @@ class ConversationFragment :
|
|||
disposables += send
|
||||
.doOnSubscribe {
|
||||
if (clearCompose) {
|
||||
composeTextEventsListener?.typingStatusEnabled = false
|
||||
composeText.setText("")
|
||||
composeTextEventsListener?.typingStatusEnabled = true
|
||||
attachmentManager.clear(GlideApp.with(this@ConversationFragment), false)
|
||||
inputPanel.clearQuote()
|
||||
}
|
||||
|
@ -1767,9 +1786,6 @@ class ConversationFragment :
|
|||
|
||||
private fun onSendComplete() {
|
||||
if (isDetached || activity?.isFinishing == true) {
|
||||
if (args.conversationScreenType.isInPopup) {
|
||||
activity?.finish()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1781,6 +1797,10 @@ class ConversationFragment :
|
|||
draftViewModel.onSendComplete()
|
||||
|
||||
inputPanel.exitEditMessageMode()
|
||||
|
||||
if (args.conversationScreenType.isInPopup) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRecentSafetyNumberChange(changedRecords: List<IdentityRecord>) {
|
||||
|
@ -2004,6 +2024,7 @@ class ConversationFragment :
|
|||
} else if (isSearchRequested) {
|
||||
searchMenuItem?.collapseActionView()
|
||||
} else if (args.conversationScreenType.isInBubble) {
|
||||
isEnabled = false
|
||||
requireActivity().onBackPressed()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.signal.core.util.logging.Log
|
|||
import org.signal.core.util.toInt
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData
|
||||
import org.thoughtcrime.securesms.conversation.ConversationDataSource
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
|
|
|
@ -123,7 +123,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator;
|
|||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
||||
import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet;
|
||||
import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationFragment;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource;
|
||||
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView;
|
||||
|
@ -169,6 +168,7 @@ import org.thoughtcrime.securesms.util.AppStartup;
|
|||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
|
@ -979,8 +979,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
requireCallback().getSearchToolbar().get();
|
||||
}
|
||||
|
||||
if (getContext() != null) {
|
||||
ConversationFragment.prepare(getContext());
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
if (SignalStore.internalValues().useConversationItemV2()) {
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_incoming, parent, 25);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_outgoing, parent, 25);
|
||||
} else {
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_text_only, parent, 25);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_text_only, parent, 25);
|
||||
}
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5);
|
||||
CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,7 @@ import android.graphics.Canvas
|
|||
import android.graphics.Rect
|
||||
import androidx.core.view.children
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
||||
import org.thoughtcrime.securesms.conversation.ConversationHeaderView
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
|
@ -28,28 +27,22 @@ class GiphyMp4ItemDecoration(
|
|||
parent.translationY = 0f
|
||||
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
|
||||
} else {
|
||||
val threadHeaderViewHolder = parent.children
|
||||
.map { parent.getChildViewHolder(it) }
|
||||
.filter { it is ConversationAdapter.FooterViewHolder || it is ConversationAdapterV2.ThreadHeaderViewHolder }
|
||||
val threadHeaderView: ConversationHeaderView? = parent.children
|
||||
.filterIsInstance<ConversationHeaderView>()
|
||||
.firstOrNull()
|
||||
|
||||
if (threadHeaderViewHolder == null) {
|
||||
if (threadHeaderView == null) {
|
||||
parent.translationY = 0f
|
||||
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
|
||||
return
|
||||
}
|
||||
|
||||
val toolbarMargin = if (threadHeaderViewHolder is ConversationAdapterV2.ThreadHeaderViewHolder) {
|
||||
// A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height
|
||||
val bounds = Rect()
|
||||
parent.getDecoratedBoundsWithMargins(threadHeaderViewHolder.itemView, bounds)
|
||||
bounds.bottom - bounds.top - threadHeaderViewHolder.itemView.height
|
||||
} else {
|
||||
// Deprecated not needed for CFv2
|
||||
0
|
||||
}
|
||||
// A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height
|
||||
val bounds = Rect()
|
||||
parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds)
|
||||
val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height
|
||||
|
||||
val childTop: Int = threadHeaderViewHolder.itemView.top - toolbarMargin
|
||||
val childTop: Int = threadHeaderView.top - toolbarMargin
|
||||
parent.translationY = min(0, -childTop).toFloat()
|
||||
onRecyclerVerticalTranslationSet?.invoke(parent.translationY)
|
||||
}
|
||||
|
|
|
@ -107,7 +107,6 @@ public final class FeatureFlags {
|
|||
private static final String MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes";
|
||||
private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch";
|
||||
private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode";
|
||||
private static final String CONVERSATION_FRAGMENT_V2 = "android.conversationFragmentV2.2";
|
||||
private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch";
|
||||
private static final String SAFETY_NUMBER_ACI = "global.safetyNumberAci";
|
||||
|
||||
|
@ -169,7 +168,6 @@ public final class FeatureFlags {
|
|||
AD_HOC_CALLING,
|
||||
SVR2_KILLSWITCH,
|
||||
CDS_DISABLE_COMPAT_MODE,
|
||||
CONVERSATION_FRAGMENT_V2,
|
||||
SAFETY_NUMBER_ACI,
|
||||
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH
|
||||
);
|
||||
|
@ -237,7 +235,6 @@ public final class FeatureFlags {
|
|||
MAX_ATTACHMENT_SIZE_BYTES,
|
||||
SVR2_KILLSWITCH,
|
||||
CDS_DISABLE_COMPAT_MODE,
|
||||
CONVERSATION_FRAGMENT_V2,
|
||||
SAFETY_NUMBER_ACI,
|
||||
FCM_MAY_HAVE_MESSAGES_KILL_SWITCH
|
||||
);
|
||||
|
@ -621,11 +618,6 @@ public final class FeatureFlags {
|
|||
}
|
||||
}
|
||||
|
||||
/** True if the new conversation fragment should be used. */
|
||||
public static boolean useConversationFragmentV2() {
|
||||
return getBoolean(CONVERSATION_FRAGMENT_V2, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -1,302 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.InsetAwareConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/system_ui_guidelines" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/conversation_wallpaper"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:importantForAccessibility="no"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
|
||||
|
||||
<View
|
||||
android:id="@+id/conversation_wallpaper_dim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:visibility="gone"
|
||||
tools:alpha="0.2f"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/keyboard_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/conversation_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/fragment_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/conversation_mention_suggestions_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout="@layout/conversation_mention_suggestions_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/attachment_editor_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/attachment_editor"
|
||||
android:layout="@layout/conversation_activity_attachment_editor_stub" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/conversation_activity_panel_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/scheduled_messages_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/scheduled_messages"
|
||||
android:layout="@layout/conversation_activity_scheduled_messages_stub" />
|
||||
|
||||
<include layout="@layout/conversation_search_nav" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<include
|
||||
layout="@layout/conversation_no_longer_a_member"
|
||||
android:visibility="gone" />
|
||||
|
||||
<include
|
||||
layout="@layout/conversation_requesting_bottom_banner"
|
||||
android:visibility="gone" />
|
||||
|
||||
<include layout="@layout/conversation_input_panel" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/conversation_cannot_send_announcement_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_cannot_send_announcement_group" />
|
||||
|
||||
<org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
|
||||
android:id="@+id/conversation_activity_message_request_bottom_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/conversation_release_notes_unmute_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_activity_unmute" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/register_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp"
|
||||
android:text="@string/conversation_activity__enable_signal_messages"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/unblock_button"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp"
|
||||
android:text="@string/ConversationActivity_unblock"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/sms_export_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/sms_export_view"
|
||||
android:layout="@layout/conversation_activity_sms_export_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/logged_out_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/logged_out_view"
|
||||
android:layout="@layout/conversation_activity_logged_out_stub" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/space_left"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:windowBackground"
|
||||
android:paddingStart="5dp"
|
||||
android:visibility="gone"
|
||||
tools:text="160/160 (1)"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/emoji_drawer_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/emoji_drawer"
|
||||
android:layout="@layout/conversation_activity_emojidrawer_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/attachment_keyboard_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/attachment_keyboard"
|
||||
android:layout="@layout/conversation_activity_attachment_keyboard_stub" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/navbar_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/wallpaper_compose_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/navigation_bar_guideline" />
|
||||
|
||||
<View
|
||||
android:id="@+id/toolbar_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/signal_m3_toolbar_height"
|
||||
android:background="@color/signal_colorBackground"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="@id/status_bar_guideline" />
|
||||
|
||||
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/signal_m3_toolbar_height"
|
||||
android:background="@color/transparent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:minHeight="@dimen/signal_m3_toolbar_height"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
app:contentInsetStart="46dp"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="@id/status_bar_guideline">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<include
|
||||
layout="@layout/conversation_title_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/conversation_group_call_join"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="@string/ConversationActivity_join"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="@color/core_white"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/core_ultramarine"
|
||||
app:cornerRadius="@dimen/material_button_full_round_corner_radius"
|
||||
app:icon="@drawable/ic_video_solid_18"
|
||||
app:iconGravity="textStart"
|
||||
app:iconTint="@color/core_white"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.DarkOverflowToolbar>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/conversation_banner_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/voice_note_player_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/voice_note_player"
|
||||
android:layout="@layout/voice_note_player_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/unverified_banner_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/unverified_banner"
|
||||
android:layout="@layout/conversation_activity_unverified_banner_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/reminder_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/reminder"
|
||||
android:layout="@layout/conversation_activity_reminderview_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/review_banner_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/review_banner"
|
||||
android:layout="@layout/review_banner_view" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/conversation_reaction_scrubber_stub"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:inflatedId="@+id/conversation_reaction_scrubber"
|
||||
android:layout="@layout/conversation_reaction_scrubber"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@+id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@+id/parent_start_guideline"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_bar_guideline" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.AttachmentKeyboard
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/attachment_keyboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/emoji_drawer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
|
@ -1,105 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/video_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@android:id/list"
|
||||
app:layout_constraintEnd_toEndOf="@android:id/list"
|
||||
app:layout_constraintStart_toStartOf="@android:id/list"
|
||||
app:layout_constraintTop_toTopOf="@android:id/list" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.mutiselect.MultiselectRecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:cacheColorHint="@color/signal_background_primary"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="@dimen/conversation_bottom_padding"
|
||||
android:scrollbars="vertical"
|
||||
android:splitMotionEvents="false"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/reactions_shade"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/reactions_screen_light_shade_color"
|
||||
android:foreground="@color/reactions_screen_dark_shade_color"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scroll_date_header"
|
||||
style="@style/Signal.Text.BodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/sticky_date_header_background"
|
||||
android:elevation="9dp"
|
||||
android:gravity="center"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textColor="@color/signal_colorOnSurfaceVariant"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="March 1, 2015" />
|
||||
|
||||
<View
|
||||
android:id="@+id/compose_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:alpha="1"
|
||||
android:background="@drawable/compose_divider_background"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationScrollToView
|
||||
android:id="@+id/scroll_to_mention"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="invisible"
|
||||
app:cstv_scroll_button_src="@drawable/ic_at_20"
|
||||
app:layout_constraintBottom_toTopOf="@id/scroll_to_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_goneMarginBottom="20dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ConversationScrollToView
|
||||
android:id="@+id/scroll_to_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="invisible"
|
||||
app:cstv_scroll_button_src="@drawable/ic_chevron_down_20"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.menu.SignalBottomActionBar
|
||||
android:id="@+id/conversation_bottom_action_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.ConversationHeaderView
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="32dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:tag="mentions_picker_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:name="org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment"/>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:viewBindingIgnore="true"
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
Loading…
Add table
Reference in a new issue