diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt
index da869e7757..d18fba5496 100644
--- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt
+++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/ConversationItemPreviewer.kt
@@ -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
diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/SafetyNumberChangeDialogPreviewer.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/SafetyNumberChangeDialogPreviewer.kt
index 7adcb2b950..597d5d5d63 100644
--- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/SafetyNumberChangeDialogPreviewer.kt
+++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/SafetyNumberChangeDialogPreviewer.kt
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ef24edc964..ad93e46ac0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -630,22 +630,11 @@
android:value="org.thoughtcrime.securesms.MainActivity" />
-
-
-
-
+ android:theme="@style/Signal.DayNight"
+ android:allowEmbedded="true"
+ android:resizeableActivity="true"
+ android:exported="false"/>
= CopyOnWriteArrayList()
private var scheduledSendListener: ScheduledSendListener? = null
private var availableSendTypes: List = 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
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.java
deleted file mode 100644
index 6adb284ad5..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.java
+++ /dev/null
@@ -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) {
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.kt
new file mode 100644
index 0000000000..2075bf034f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/BubbleConversationActivity.kt
@@ -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))
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt
deleted file mode 100644
index e109b5bf3f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.kt
+++ /dev/null
@@ -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 {
- return fragment.reminderView
- }
-
- override val stripeRepository: StripeRepository by lazy { StripeRepository(this) }
- override val googlePayResultPublisher: Subject = PublishSubject.create()
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java
deleted file mode 100644
index 6bf89e7034..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java
+++ /dev/null
@@ -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 {
-
- 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 load(int start, int length, int totalSize, @NonNull CancellationSignal cancellationSignal) {
- Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), thread " + threadId);
- List 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 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 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 mentions = SignalDatabase.mentions().getMentionsForMessage(messageId.getId());
- stopwatch.split("mentions");
-
- boolean isQuoted = SignalDatabase.messages().isQuoted(record);
- stopwatch.split("is-quoted");
-
- List reactions = SignalDatabase.reactions().getReactions(messageId);
- record = ReactionHelper.recordWithReactions(record, reactions);
- stopwatch.split("reactions");
-
- List 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());
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
deleted file mode 100644
index 7a14582a5e..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java
+++ /dev/null
@@ -1,2368 +0,0 @@
-/*
- * Copyright (C) 2015 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.thoughtcrime.securesms.conversation;
-
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.app.ActivityOptions;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.ViewSwitcher;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.view.ActionMode;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.app.ActivityCompat;
-import androidx.core.app.ActivityOptionsCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.text.HtmlCompat;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.ViewKt;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
-
-import com.annimon.stream.Collectors;
-import com.annimon.stream.Stream;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.snackbar.Snackbar;
-import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback;
-
-import org.jetbrains.annotations.NotNull;
-import org.signal.core.util.DimensionUnit;
-import org.signal.core.util.Stopwatch;
-import org.signal.core.util.StreamUtil;
-import org.signal.core.util.concurrent.LifecycleDisposable;
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.concurrent.SimpleTask;
-import org.signal.core.util.logging.Log;
-import org.signal.ringrtc.CallLinkRootKey;
-import org.thoughtcrime.securesms.BindableConversationItem;
-import org.thoughtcrime.securesms.LoggingFragment;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.badges.gifts.OpenableGift;
-import org.thoughtcrime.securesms.badges.gifts.OpenableGiftItemDecoration;
-import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity;
-import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet;
-import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet;
-import org.thoughtcrime.securesms.components.ConversationScrollToView;
-import org.thoughtcrime.securesms.components.ConversationTypingView;
-import org.thoughtcrime.securesms.components.TypingStatusRepository;
-import org.thoughtcrime.securesms.components.menu.ActionItem;
-import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
-import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
-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.voice.VoiceNoteMediaControllerOwner;
-import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
-import org.thoughtcrime.securesms.contactshare.Contact;
-import org.thoughtcrime.securesms.contactshare.ContactUtil;
-import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
-import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
-import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
-import org.thoughtcrime.securesms.conversation.colors.Colorizer;
-import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
-import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator;
-import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration;
-import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
-import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet;
-import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
-import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
-import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet;
-import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
-import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
-import org.thoughtcrime.securesms.conversation.v2.AddToContactsContract;
-import org.thoughtcrime.securesms.conversation.v2.BubbleLayoutTransitionListener;
-import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs;
-import org.thoughtcrime.securesms.database.DatabaseObserver;
-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.MessageId;
-import org.thoughtcrime.securesms.database.model.MessageRecord;
-import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration;
-import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
-import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
-import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
-import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
-import org.thoughtcrime.securesms.groups.ui.GroupErrors;
-import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
-import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult;
-import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
-import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
-import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.linkpreview.LinkPreview;
-import org.thoughtcrime.securesms.longmessage.LongMessageFragment;
-import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
-import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
-import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity;
-import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
-import org.thoughtcrime.securesms.mms.AttachmentManager;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.OutgoingMessage;
-import org.thoughtcrime.securesms.mms.PartAuthority;
-import org.thoughtcrime.securesms.mms.Slide;
-import org.thoughtcrime.securesms.mms.TextSlide;
-import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile;
-import org.thoughtcrime.securesms.notifications.v2.ConversationId;
-import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
-import org.thoughtcrime.securesms.providers.BlobProvider;
-import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
-import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.recipients.LiveRecipient;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientExporter;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.revealable.ViewOnceMessageActivity;
-import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
-import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
-import org.thoughtcrime.securesms.sms.MessageSender;
-import org.thoughtcrime.securesms.stickers.StickerLocator;
-import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
-import org.thoughtcrime.securesms.stories.Stories;
-import org.thoughtcrime.securesms.stories.StoryViewerArgs;
-import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity;
-import org.thoughtcrime.securesms.util.CachedInflater;
-import org.thoughtcrime.securesms.util.CommunicationActions;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.HtmlUtil;
-import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
-import org.thoughtcrime.securesms.util.MessageRecordUtil;
-import org.thoughtcrime.securesms.util.Projection;
-import org.thoughtcrime.securesms.util.SaveAttachmentTask;
-import org.thoughtcrime.securesms.util.SignalLocalMetrics;
-import org.thoughtcrime.securesms.util.SignalProxyUtil;
-import org.thoughtcrime.securesms.util.SignalTrace;
-import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
-import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
-import org.thoughtcrime.securesms.util.StorageUtil;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.TopToastPopup;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.ViewExtensionsKt;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.WindowUtil;
-import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
-import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import kotlin.Unit;
-
-@SuppressLint("StaticFieldLeak")
-public class ConversationFragment extends LoggingFragment implements MultiselectForwardBottomSheet.Callback, ConversationBottomSheetCallback {
- private static final String TAG = Log.tag(ConversationFragment.class);
-
- private static final int SCROLL_ANIMATION_THRESHOLD = 50;
- private static final int MAX_SCROLL_DELAY_COUNT = 5;
-
- private final ActionModeCallback actionModeCallback = new ActionModeCallback();
- private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
- private final LifecycleDisposable disposables = new LifecycleDisposable();
- private final LifecycleDisposable lastSeenDisposable = new LifecycleDisposable();
-
- private ConversationFragmentListener listener;
-
- private LiveRecipient recipient;
- private long threadId;
- private ActionMode actionMode;
- private Locale locale;
- private FrameLayout videoContainer;
- private RecyclerView list;
- private LastSeenHeader lastSeenDecoration;
- private RecyclerView.ItemDecoration inlineDateDecoration;
- private ViewSwitcher topLoadMoreView;
- private ViewSwitcher bottomLoadMoreView;
- private ConversationTypingView typingView;
- private View composeDivider;
- private ConversationScrollToView scrollToBottomButton;
- private ConversationScrollToView scrollToMentionButton;
- private TextView scrollDateHeader;
- private ConversationHeaderView conversationHeader;
- private MessageRequestViewModel messageRequestViewModel;
- private MessageCountsViewModel messageCountsViewModel;
- private ConversationViewModel conversationViewModel;
- private ConversationGroupViewModel groupViewModel;
- private SnapToTopDataObserver snapToTopDataObserver;
- private MarkReadHelper markReadHelper;
- private OnScrollListener conversationScrollListener;
- private int lastSeenScrollOffset;
- private Stopwatch startupStopwatch;
- private View reactionsShade;
- private SignalBottomActionBar bottomActionBar;
- private OpenableGiftItemDecoration openableGiftItemDecoration;
-
- private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
- private Colorizer colorizer;
- private ConversationUpdateTick conversationUpdateTick;
- private MultiselectItemDecoration multiselectItemDecoration;
-
- private @Nullable ConversationData conversationData;
- private @Nullable ChatWallpaper chatWallpaper;
-
- private final DatabaseObserver.Observer threadDeletedObserver = this::onThreadDelete;
-
- private ActivityResultLauncher addToContactsLauncher;
-
- public static void prepare(@NonNull Context context) {
- FrameLayout parent = new FrameLayout(context);
- parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT));
-
- if (FeatureFlags.useConversationFragmentV2() && 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);
- }
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- this.locale = Locale.getDefault();
- startupStopwatch = new Stopwatch("conversation-open");
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
- addToContactsLauncher = registerForActivityResult(new AddToContactsContract(), result -> {});
-
- disposables.bindTo(getViewLifecycleOwner());
- lastSeenDisposable.bindTo(getViewLifecycleOwner());
-
- final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
- videoContainer = view.findViewById(R.id.video_container);
- list = view.findViewById(android.R.id.list);
- composeDivider = view.findViewById(R.id.compose_divider);
-
- BubbleLayoutTransitionListener bubbleLayoutTransitionListener = new BubbleLayoutTransitionListener(list);
- getViewLifecycleOwner().getLifecycle().addObserver(bubbleLayoutTransitionListener);
-
- scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
- scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
- scrollDateHeader = view.findViewById(R.id.scroll_date_header);
- reactionsShade = view.findViewById(R.id.reactions_shade);
- bottomActionBar = view.findViewById(R.id.conversation_bottom_action_bar);
-
- final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
- final ConversationItemAnimator conversationItemAnimator = new ConversationItemAnimator(
- () -> {
- ConversationAdapter adapter = getListAdapter();
- if (adapter == null) {
- return false;
- } else {
- return Util.hasItems(adapter.getSelectedItems());
- }
- },
- () -> conversationViewModel.shouldPlayMessageAnimations() && list.getScrollState() == RecyclerView.SCROLL_STATE_IDLE,
- () -> list.canScrollVertically(1) || list.canScrollVertically(-1),
- (viewHolder) -> {
- if (viewHolder instanceof ConversationAdapter.ConversationViewHolder) {
- ConversationAdapter.ConversationViewHolder conversationViewHolder = (ConversationAdapter.ConversationViewHolder) viewHolder;
- BindableConversationItem conversationItem = conversationViewHolder.getBindable();
- if (conversationItem != null) {
- return !MessageRecordUtil.isEditMessage(conversationItem.getConversationMessage().getMessageRecord());
- }
- }
- return true;
- });
-
- multiselectItemDecoration = new MultiselectItemDecoration(requireContext(), () -> chatWallpaper);
-
- list.setHasFixedSize(false);
- list.setLayoutManager(layoutManager);
-
- RecyclerViewColorizer recyclerViewColorizer = new RecyclerViewColorizer(list);
-
- openableGiftItemDecoration = new OpenableGiftItemDecoration(requireContext());
- getViewLifecycleOwner().getLifecycle().addObserver(openableGiftItemDecoration);
-
- list.addItemDecoration(openableGiftItemDecoration);
- list.addItemDecoration(multiselectItemDecoration);
- list.setItemAnimator(conversationItemAnimator);
-
- ((Material3OnScrollHelperBinder) requireParentFragment()).bindScrollHelper(list);
-
- getViewLifecycleOwner().getLifecycle().addObserver(multiselectItemDecoration);
-
- snapToTopDataObserver = new ConversationSnapToTopDataObserver(list, new ConversationScrollRequestValidator());
- conversationHeader = (ConversationHeaderView) inflater.inflate(R.layout.conversation_item_banner, container, false);
- topLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
- bottomLoadMoreView = (ViewSwitcher) inflater.inflate(R.layout.load_more_header, container, false);
-
- initializeLoadMoreView(topLoadMoreView);
- initializeLoadMoreView(bottomLoadMoreView);
-
- typingView = (ConversationTypingView) inflater.inflate(R.layout.conversation_typing_view, container, false);
-
- new ConversationItemSwipeCallback(
- conversationMessage -> actionMode == null &&
- MenuState.canReplyToMessage(recipient.get(),
- MenuState.isActionMessage(conversationMessage.getMessageRecord()),
- conversationMessage.getMessageRecord(),
- messageRequestViewModel.shouldShowMessageRequest(),
- groupViewModel.isNonAdminInAnnouncementGroup()),
- this::handleReplyMessage
- ).attachToRecyclerView(list);
-
- giphyMp4ProjectionRecycler = initializeGiphyMp4();
-
- this.groupViewModel = new ViewModelProvider(getParentFragment(), (ViewModelProvider.Factory) new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
- this.messageCountsViewModel = new ViewModelProvider(getParentFragment()).get(MessageCountsViewModel.class);
- this.conversationViewModel = new ViewModelProvider(getParentFragment(), (ViewModelProvider.Factory) new ConversationViewModel.Factory()).get(ConversationViewModel.class);
-
- disposables.add(conversationViewModel.getChatColors().subscribe(chatColors -> {
- recyclerViewColorizer.setChatColors(chatColors);
- scrollToMentionButton.setUnreadCountBackgroundTint(chatColors.asSingleColor());
- scrollToBottomButton.setUnreadCountBackgroundTint(chatColors.asSingleColor());
- }));
-
- disposables.add(conversationViewModel.getMessageData().subscribe(messageData -> {
- SignalLocalMetrics.ConversationOpen.onDataPostedToMain();
-
- ConversationAdapter adapter = getListAdapter();
- if (adapter != null) {
- final AtomicBoolean firstRender = new AtomicBoolean(true);
- List messages = messageData.getMessages();
- getListAdapter().submitList(messages, () -> {
-
- if (firstRender.get()) {
- firstRender.set(false);
- ViewExtensionsKt.doAfterNextLayout(list, () -> {
- startupStopwatch.split("first-render");
- startupStopwatch.stop(TAG);
- SignalLocalMetrics.ConversationOpen.onRenderFinished();
- listener.onFirstRender();
- return Unit.INSTANCE;
- });
- }
-
- list.post(() -> {
- conversationViewModel.onMessagesCommitted(messages);
- });
- });
- }
-
- presentConversationMetadata(messageData.getMetadata());
- }));
-
- disposables.add(conversationViewModel.getWallpaper().subscribe(w -> {
- chatWallpaper = w.orElse(null);
- scrollToBottomButton.setWallpaperEnabled(w.isPresent());
- scrollToMentionButton.setWallpaperEnabled(w.isPresent());
- }));
-
- conversationViewModel.getShowMentionsButton().observe(getViewLifecycleOwner(), shouldShow -> {
- scrollToMentionButton.setShown(shouldShow);
- });
-
- conversationViewModel.getShowScrollToBottom().observe(getViewLifecycleOwner(), shouldShow -> {
- scrollToBottomButton.setShown(shouldShow);
- });
-
- scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
- scrollToMentionButton.setOnClickListener(v -> scrollToNextMention());
-
- updateToolbarDependentMargins();
-
- colorizer = new Colorizer();
- disposables.add(conversationViewModel.getNameColorsMap().subscribe(nameColorsMap -> {
- colorizer.onNameColorsChanged(nameColorsMap);
-
- ConversationAdapter adapter = getListAdapter();
- if (adapter != null) {
- adapter.notifyItemRangeChanged(0, adapter.getItemCount(), ConversationAdapter.PAYLOAD_NAME_COLORS);
- }
- }));
-
- conversationUpdateTick = new ConversationUpdateTick(this::updateConversationItemTimestamps);
- getViewLifecycleOwner().getLifecycle().addObserver(conversationUpdateTick);
-
- listener.getVoiceNoteMediaController().getVoiceNotePlayerViewState().observe(getViewLifecycleOwner(), state -> conversationViewModel.setInlinePlayerVisible(state.isPresent()));
- conversationViewModel.getConversationTopMargin().observe(getViewLifecycleOwner(), topMargin -> {
- lastSeenScrollOffset = topMargin;
- ViewUtil.setTopMargin(scrollDateHeader, topMargin + ViewUtil.dpToPx(8));
- });
-
- conversationViewModel.getActiveNotificationProfile().observe(getViewLifecycleOwner(), this::updateNotificationProfileStatus);
-
- initializeResources();
- initializeMessageRequestViewModel();
- initializeListAdapter();
-
- conversationViewModel.getSearchQuery().observe(getViewLifecycleOwner(), this::onSearchQueryUpdated);
-
- disposables.add(conversationViewModel.getMarkReadRequests()
- .subscribe(timeSince -> markReadHelper.onViewsRevealed(timeSince)));
-
- return view;
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- getChildFragmentManager().setFragmentResultListener(ViewReceivedGiftBottomSheet.REQUEST_KEY, getViewLifecycleOwner(), (key, bundle) -> {
- if (bundle.getBoolean(ViewReceivedGiftBottomSheet.RESULT_NOT_NOW, false)) {
- Snackbar.make(view.getRootView(), R.string.ConversationFragment__you_can_redeem_your_badge_later, Snackbar.LENGTH_SHORT)
- .show();
- }
- });
- }
-
- @Override
- public void onDestroy() {
- ApplicationDependencies.getDatabaseObserver().unregisterObserver(threadDeletedObserver);
- super.onDestroy();
- }
-
- private @NonNull GiphyMp4ProjectionRecycler initializeGiphyMp4() {
- int maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation();
- List holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(requireContext(),
- getViewLifecycleOwner().getLifecycle(),
- videoContainer,
- maxPlayback);
- GiphyMp4ProjectionRecycler callback = new GiphyMp4ProjectionRecycler(holders);
-
- GiphyMp4PlaybackController.attach(list, callback, maxPlayback);
- list.addItemDecoration(new GiphyMp4ItemDecoration(callback, translationY -> {
- reactionsShade.setTranslationY(translationY + list.getHeight());
- return Unit.INSTANCE;
- }), 0);
-
- return callback;
- }
-
- public void clearFocusedItem() {
- multiselectItemDecoration.setFocusedItem(null);
- list.invalidateItemDecorations();
- }
-
- private void updateConversationItemTimestamps() {
- ConversationAdapter conversationAdapter = getListAdapter();
- if (conversationAdapter != null) {
- getListAdapter().updateTimestamps();
- }
- }
-
- @Override
- public void onAttach(Context context) {
- super.onAttach(context);
- this.listener = (ConversationFragmentListener) getParentFragment();
- }
-
- @Override
- public void onStart() {
- super.onStart();
- initializeTypingObserver();
- SignalProxyUtil.startListeningToWebsocket();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- int lastVisiblePosition = getListLayoutManager().findLastVisibleItemPosition();
- int firstVisiblePosition = getListLayoutManager().findFirstCompletelyVisibleItemPosition();
-
- final long lastVisibleMessageTimestamp;
- if (firstVisiblePosition > 0 && lastVisiblePosition != RecyclerView.NO_POSITION) {
- ConversationMessage message = getListAdapter().getLastVisibleConversationMessage(lastVisiblePosition);
-
- lastVisibleMessageTimestamp = message != null ? message.getMessageRecord().getDateReceived() : 0;
- } else {
- lastVisibleMessageTimestamp = 0;
- }
- SignalExecutors.BOUNDED.submit(() -> SignalDatabase.threads().setLastScrolled(threadId, lastVisibleMessageTimestamp));
- }
-
- @Override
- public void onStop() {
- super.onStop();
- ApplicationDependencies.getTypingStatusRepository().getTypists(threadId).removeObservers(getViewLifecycleOwner());
- }
-
- @Override
- public void onConfigurationChanged(@NonNull Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- updateToolbarDependentMargins();
- }
-
- public void moveToLastSeen() {
- int lastSeenPosition = conversationData != null ? conversationData.getLastSeenPosition() : 0;
- if (lastSeenPosition <= 0) {
- Log.i(TAG, "No need to move to last seen.");
- return;
- }
-
- if (list == null || getListAdapter() == null) {
- Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet.");
- return;
- }
-
- int position = getListAdapter().getAdapterPositionForMessagePosition(lastSeenPosition);
- snapToTopDataObserver.requestScrollPosition(position);
- }
-
- public void onWallpaperChanged(@Nullable ChatWallpaper wallpaper) {
- if (scrollDateHeader != null) {
- scrollDateHeader.setBackgroundResource(wallpaper != null ? R.drawable.sticky_date_header_background_wallpaper
- : R.drawable.sticky_date_header_background);
- scrollDateHeader.setTextColor(ContextCompat.getColor(requireContext(), wallpaper != null ? R.color.sticky_header_foreground_wallpaper
- : R.color.signal_colorOnSurfaceVariant));
- }
-
- if (list != null) {
- ConversationAdapter adapter = getListAdapter();
-
- if (adapter != null) {
- Log.d(TAG, "Notifying adapter that wallpaper state has changed.");
-
- if (adapter.onHasWallpaperChanged(wallpaper != null)) {
- setInlineDateDecoration(adapter);
- }
- }
- }
- }
-
- private int getStartPosition() {
- return conversationViewModel.getArgs().getStartingPosition();
- }
-
- private void initializeMessageRequestViewModel() {
- MessageRequestViewModel.Factory factory = new MessageRequestViewModel.Factory(requireContext());
-
- messageRequestViewModel = new ViewModelProvider(requireParentFragment(), factory).get(MessageRequestViewModel.class);
- messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
-
- listener.onMessageRequest(messageRequestViewModel);
-
- messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
- presentMessageRequestProfileView(requireContext(), recipientInfo, conversationHeader);
- });
-
- messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
- ConversationAdapter adapter = getListAdapter();
- if (adapter != null) {
- adapter.setMessageRequestAccepted(data.getMessageState() == MessageRequestState.NONE);
- }
- });
- }
-
- private void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationHeaderView conversationBanner) {
- if (conversationBanner == null) {
- return;
- }
-
- Recipient recipient = recipientInfo.getRecipient();
- boolean isSelf = Recipient.self().equals(recipient);
- int memberCount = recipientInfo.getGroupMemberCount();
- int pendingMemberCount = recipientInfo.getGroupPendingMemberCount();
- List groups = recipientInfo.getSharedGroups();
-
- conversationBanner.setBadge(recipient);
-
- if (recipient != null) {
- conversationBanner.setAvatar(GlideApp.with(context), recipient);
- conversationBanner.showBackgroundBubble(recipient.hasWallpaper());
-
- String title = conversationBanner.setTitle(recipient);
- conversationBanner.setAbout(recipient);
-
- if (recipient.isGroup()) {
- if (pendingMemberCount > 0) {
- String invited = context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_invited, pendingMemberCount, pendingMemberCount);
- conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, memberCount, memberCount, invited));
- } else if (memberCount > 0) {
- conversationBanner.setSubtitle(context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_members, memberCount,
- memberCount));
- } else {
- conversationBanner.setSubtitle(null);
- }
- } else if (isSelf) {
- conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation));
- } else {
- String subtitle = recipient.getE164().map(PhoneNumberFormatter::prettyPrint).orElse(null);
-
- if (subtitle == null || subtitle.equals(title)) {
- conversationBanner.hideSubtitle();
- } else {
- conversationBanner.setSubtitle(subtitle);
- }
- }
- }
-
- if (groups.isEmpty() || isSelf) {
- if (TextUtils.isEmpty(recipientInfo.getGroupDescription())) {
- conversationBanner.setLinkifyDescription(false);
- conversationBanner.hideDescription();
- } else {
- conversationBanner.setLinkifyDescription(true);
- boolean linkifyWebLinks = recipientInfo.getMessageRequestState() == MessageRequestState.NONE;
- conversationBanner.showDescription();
- GroupDescriptionUtil.setText(context,
- conversationBanner.getDescription(),
- recipientInfo.getGroupDescription(),
- linkifyWebLinks,
- () -> GroupDescriptionDialog.show(getChildFragmentManager(),
- recipient.getDisplayName(context),
- recipientInfo.getGroupDescription(),
- linkifyWebLinks));
- }
- } else {
- final String description;
-
- switch (groups.size()) {
- case 1:
- description = context.getString(R.string.MessageRequestProfileView_member_of_one_group, HtmlUtil.bold(groups.get(0)));
- break;
- case 2:
- description = context.getString(R.string.MessageRequestProfileView_member_of_two_groups, HtmlUtil.bold(groups.get(0)), HtmlUtil.bold(groups.get(1)));
- break;
- case 3:
- description = context.getString(R.string.MessageRequestProfileView_member_of_many_groups, HtmlUtil.bold(groups.get(0)), HtmlUtil.bold(groups.get(1)), HtmlUtil.bold(groups.get(2)));
- break;
- default:
- int others = groups.size() - 2;
- description = context.getString(R.string.MessageRequestProfileView_member_of_many_groups,
- HtmlUtil.bold(groups.get(0)),
- HtmlUtil.bold(groups.get(1)),
- context.getResources().getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others));
- }
-
- conversationBanner.setDescription(HtmlCompat.fromHtml(description, 0));
- conversationBanner.showDescription();
- }
- }
-
- private void initializeResources() {
- long oldThreadId = threadId;
- int startingPosition = getStartPosition();
-
- this.recipient = Recipient.live(conversationViewModel.getArgs().getRecipientId());
- setThreadId(conversationViewModel.getArgs().getThreadId());
- this.markReadHelper = new MarkReadHelper(ConversationId.forConversation(threadId), requireContext(), getViewLifecycleOwner());
-
- conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, startingPosition);
- messageCountsViewModel.setThreadId(threadId);
-
- messageCountsViewModel.getUnreadMessagesCount().observe(getViewLifecycleOwner(), scrollToBottomButton::setUnreadCount);
- messageCountsViewModel.getUnreadMentionsCount().observe(getViewLifecycleOwner(), count -> {
- scrollToMentionButton.setUnreadCount(count);
- conversationViewModel.setHasUnreadMentions(count > 0);
- });
-
- conversationScrollListener = new ConversationScrollListener(requireContext());
- list.addOnScrollListener(conversationScrollListener);
-
- if (oldThreadId != threadId) {
- ApplicationDependencies.getTypingStatusRepository().getTypists(oldThreadId).removeObservers(getViewLifecycleOwner());
- }
- }
-
- private void setThreadId(long threadId) {
- this.threadId = threadId;
- ApplicationDependencies.getDatabaseObserver().unregisterObserver(threadDeletedObserver);
- ApplicationDependencies.getDatabaseObserver().registerConversationDeleteObserver(this.threadId, threadDeletedObserver);
- }
-
- private void initializeListAdapter() {
- if (this.recipient != null) {
- if (getListAdapter() != null && getListAdapter().isForRecipientId(this.recipient.getId())) {
- Log.d(TAG, "List adapter already initialized for " + this.recipient.getId());
- return;
- }
-
- Log.d(TAG, "Initializing adapter for " + recipient.getId());
- ConversationAdapter adapter = new ConversationAdapter(requireContext(), this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get().hasWallpaper(), colorizer);
- adapter.setPagingController(conversationViewModel.getPagingController());
- list.setAdapter(adapter);
- setInlineDateDecoration(adapter);
- ConversationAdapter.initializePool(list.getRecycledViewPool());
-
- adapter.registerAdapterDataObserver(snapToTopDataObserver);
- adapter.registerAdapterDataObserver(new CheckExpirationDataObserver());
-
- setLastSeen(conversationData != null ? conversationData.getLastSeen() : 0);
-
- adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
- @Override
- public void onItemRangeInserted(int positionStart, int itemCount) {
- adapter.unregisterAdapterDataObserver(this);
- startupStopwatch.split("data-set");
- }
- });
- }
- }
-
- private void initializeLoadMoreView(ViewSwitcher loadMoreView) {
- loadMoreView.setOnClickListener(v -> {
- loadMoreView.showNext();
- loadMoreView.setOnClickListener(null);
- });
- }
-
- private void initializeTypingObserver() {
- if (!TextSecurePreferences.isTypingIndicatorsEnabled(requireContext())) {
- return;
- }
-
- LiveData typists = ApplicationDependencies.getTypingStatusRepository().getTypists(threadId);
-
- typists.removeObservers(getViewLifecycleOwner());
- typists.observe(getViewLifecycleOwner(), typingState -> {
- List recipients;
- boolean replacedByIncomingMessage;
-
- if (typingState != null) {
- recipients = typingState.getTypists();
- replacedByIncomingMessage = typingState.isReplacedByIncomingMessage();
- } else {
- recipients = Collections.emptyList();
- replacedByIncomingMessage = false;
- }
-
- Recipient resolved = recipient.get();
- typingView.setTypists(GlideApp.with(ConversationFragment.this), recipients, resolved.isGroup(), resolved.hasWallpaper());
-
- ConversationAdapter adapter = getListAdapter();
- adapter.setTypingView(typingView);
-
- if (recipients.size() > 0) {
- if (!isTypingIndicatorShowing() && isAtBottom()) {
- adapter.setTypingViewEnabled(true);
- list.scrollToPosition(0);
- } else {
- adapter.setTypingViewEnabled(true);
- }
- } else {
- if (isTypingIndicatorShowing() && getListLayoutManager().findFirstCompletelyVisibleItemPosition() == 0 && getListLayoutManager().getItemCount() > 1 && !replacedByIncomingMessage) {
- adapter.setTypingViewEnabled(false);
- } else if (!replacedByIncomingMessage) {
- adapter.setTypingViewEnabled(false);
- } else {
- adapter.setTypingViewEnabled(false);
- }
- }
- });
- }
-
- private void setCorrectActionModeMenuVisibility() {
- Set selectedParts = getListAdapter().getSelectedItems();
-
- if (actionMode != null && selectedParts.size() == 0) {
- actionMode.finish();
- return;
- }
-
- setBottomActionBarVisibility(true);
-
- MenuState menuState = MenuState.getMenuState(recipient.get(), selectedParts, messageRequestViewModel.shouldShowMessageRequest(), groupViewModel.isNonAdminInAnnouncementGroup());
-
- List items = new ArrayList<>();
-
- if (menuState.shouldShowReplyAction()) {
- items.add(new ActionItem(R.drawable.symbol_reply_24, getResources().getString(R.string.conversation_selection__menu_reply), () -> {
- maybeShowSwipeToReplyTooltip();
- handleReplyMessage(getSelectedConversationMessage());
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- if (menuState.shouldShowEditAction() && FeatureFlags.editMessageSending()) {
- items.add(new ActionItem(R.drawable.symbol_edit_24, getResources().getString(R.string.conversation_selection__menu_edit), () -> {
- handleEditMessage(getSelectedConversationMessage());
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- if (menuState.shouldShowForwardAction()) {
- items.add(new ActionItem(R.drawable.symbol_forward_24, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleForwardMessageParts(selectedParts)));
- }
-
- if (menuState.shouldShowSaveAttachmentAction()) {
- items.add(new ActionItem(R.drawable.symbol_save_android_24, getResources().getString(R.string.conversation_selection__menu_save), () -> {
- handleSaveAttachment((MediaMmsMessageRecord) getSelectedConversationMessage().getMessageRecord());
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- if (menuState.shouldShowCopyAction()) {
- items.add(new ActionItem(R.drawable.symbol_copy_android_24, getResources().getString(R.string.conversation_selection__menu_copy), () -> {
- handleCopyMessage(selectedParts);
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- if (menuState.shouldShowDetailsAction()) {
- items.add(new ActionItem(R.drawable.symbol_info_24, getResources().getString(R.string.conversation_selection__menu_message_details), () -> {
- handleDisplayDetails(getSelectedConversationMessage());
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- if (menuState.shouldShowDeleteAction()) {
- items.add(new ActionItem(R.drawable.symbol_trash_24, getResources().getString(R.string.conversation_selection__menu_delete), () -> {
- handleDeleteMessages(selectedParts);
- if (actionMode != null) {
- actionMode.finish();
- }
- }));
- }
-
- bottomActionBar.setItems(items);
- }
-
- private void setBottomActionBarVisibility(boolean isVisible) {
- boolean isCurrentlyVisible = bottomActionBar.getVisibility() == View.VISIBLE;
- if (isVisible == isCurrentlyVisible) {
- return;
- }
-
- int additionalScrollOffset = (int) DimensionUnit.DP.toPixels(54);
-
- if (isVisible) {
- ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
- listener.onBottomActionBarVisibilityChanged(View.VISIBLE);
-
- bottomActionBar.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- if (bottomActionBar.getHeight() == 0 && bottomActionBar.getVisibility() == View.VISIBLE) {
- return false;
- }
-
- bottomActionBar.getViewTreeObserver().removeOnPreDrawListener(this);
-
- int bottomPadding = bottomActionBar.getHeight() + (int) DimensionUnit.DP.toPixels(18);
- list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), bottomPadding);
-
- list.scrollBy(0, -(bottomPadding - additionalScrollOffset));
-
- return false;
- }
- });
- } else {
- ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation())
- .addListener(new ListenableFuture.Listener() {
- @Override public void onSuccess(Boolean result) {
- int scrollOffset = list.getPaddingBottom() - additionalScrollOffset;
- listener.onBottomActionBarVisibilityChanged(View.GONE);
- list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), getResources().getDimensionPixelSize(R.dimen.conversation_bottom_padding));
-
- ViewKt.doOnPreDraw(list, view -> {
- list.scrollBy(0, scrollOffset);
- return Unit.INSTANCE;
- });
- }
-
- @Override public void onFailure(ExecutionException e) {
- }
- });
- }
- }
-
- private @Nullable ConversationAdapter getListAdapter() {
- return (ConversationAdapter) list.getAdapter();
- }
-
- private SmoothScrollingLinearLayoutManager getListLayoutManager() {
- return (SmoothScrollingLinearLayoutManager) list.getLayoutManager();
- }
-
- private ConversationMessage getSelectedConversationMessage() {
- Set messageRecords = Stream.of(getListAdapter().getSelectedItems())
- .map(MultiselectPart::getConversationMessage)
- .distinct()
- .collect(Collectors.toSet());
-
- if (messageRecords.size() == 1) return messageRecords.stream().findFirst().get();
- else throw new AssertionError();
- }
-
- public void reload(Recipient recipient, long threadId) {
- Log.d(TAG, "[reload] Recipient: " + recipient.getId() + ", ThreadId: " + threadId);
- this.recipient = recipient.live();
-
- if (this.threadId != threadId) {
- Log.i(TAG, "ThreadId changed from " + this.threadId + " to " + threadId + ". Recipient was " + this.recipient.getId() + " and is now " + recipient.getId());
-
- setThreadId(threadId);
- messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
-
- snapToTopDataObserver.requestScrollPosition(0);
- conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
- messageCountsViewModel.setThreadId(threadId);
- markReadHelper = new MarkReadHelper(ConversationId.forConversation(threadId), requireContext(), getViewLifecycleOwner());
- initializeListAdapter();
- initializeTypingObserver();
- }
- }
-
- public void scrollToBottom() {
- if (getListLayoutManager().findFirstVisibleItemPosition() < SCROLL_ANIMATION_THRESHOLD) {
- Log.d(TAG, "scrollToBottom: Smooth scrolling to bottom of screen.");
- list.smoothScrollToPosition(0);
- } else {
- Log.d(TAG, "scrollToBottom: Scrolling to bottom of screen.");
- list.scrollToPosition(0);
- }
- }
-
- public void setInlineDateDecoration(@NonNull ConversationAdapter adapter) {
- if (inlineDateDecoration != null) {
- list.removeItemDecoration(inlineDateDecoration);
- }
-
- inlineDateDecoration = new StickyHeaderDecoration(adapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE);
- list.addItemDecoration(inlineDateDecoration, 0);
- }
-
- public void setLastSeen(long lastSeen) {
- lastSeenDisposable.clear();
- if (lastSeenDecoration != null) {
- list.removeItemDecoration(lastSeenDecoration);
- }
-
- lastSeenDecoration = new LastSeenHeader(getListAdapter(), lastSeen);
- list.addItemDecoration(lastSeenDecoration, 0);
-
- if (lastSeen > 0) {
- lastSeenDisposable.add(conversationViewModel.getThreadUnreadCount(lastSeen)
- .distinctUntilChanged()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(unreadCount -> {
- lastSeenDecoration.setUnreadCount(unreadCount);
- list.invalidateItemDecorations();
- }));
- }
- }
-
- private void handleCopyMessage(final Set multiselectParts) {
- SimpleTask.run(() -> extractBodies(multiselectParts),
- bodies -> {
- if (!Util.isEmpty(bodies)) {
- Util.copyToClipboard(requireContext(), bodies);
- }
- });
- }
-
- private @NotNull CharSequence extractBodies(final Set multiselectParts) {
- return Stream.of(multiselectParts)
- .sortBy(m -> m.getMessageRecord().getDateReceived())
- .map(MultiselectPart::getConversationMessage)
- .distinct()
- .map(message -> {
- if (MessageRecordUtil.hasTextSlide(message.getMessageRecord())) {
- TextSlide textSlide = MessageRecordUtil.requireTextSlide(message.getMessageRecord());
- if (textSlide.getUri() == null) {
- return message.getDisplayBody(requireContext());
- }
-
- try (InputStream stream = PartAuthority.getAttachmentStream(requireContext(), textSlide.getUri())) {
- String body = StreamUtil.readFullyAsString(stream);
- return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(requireContext(), message.getMessageRecord(), body, message.getThreadRecipient())
- .getDisplayBody(requireContext());
- } catch (IOException e) {
- Log.w(TAG, "Failed to read text slide data.");
- }
- }
-
- return message.getDisplayBody(requireContext());
- })
- .filterNot(Util::isEmpty)
- .collect(SpannableStringBuilder::new, (bodyBuilder, body) -> {
- if (bodyBuilder.length() > 0) {
- bodyBuilder.append('\n');
- }
- bodyBuilder.append(body);
- });
- }
-
- private void handleDeleteMessages(final Set multiselectParts) {
- Set messageRecords = Stream.of(multiselectParts).map(MultiselectPart::getMessageRecord).collect(Collectors.toSet());
- buildRemoteDeleteConfirmationDialog(messageRecords).show();
- }
-
- private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set messageRecords) {
- int messagesCount = messageRecords.size();
- AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
-
- builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
- builder.setCancelable(true);
-
- boolean isNoteToSelfDelete = isNoteToSelfDelete(messageRecords);
-
- int deleteForMeResId = isNoteToSelfDelete ? R.string.ConversationFragment_delete_on_this_device : R.string.ConversationFragment_delete_for_me;
- builder.setPositiveButton(deleteForMeResId, (dialog, which) -> {
- new ProgressDialogAsyncTask(getActivity(),
- R.string.ConversationFragment_deleting,
- R.string.ConversationFragment_deleting_messages)
- {
- @Override
- protected Void doInBackground(Void... voids) {
- for (MessageRecord messageRecord : messageRecords) {
- SignalDatabase.messages().deleteMessage(messageRecord.getId());
- }
-
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-
- for (MessageRecord messageRecord : messageRecords) {
- listener.onDeleteMessage(messageRecord.getId());
- }
- });
-
- int deleteForEveryoneResId = isNoteToSelfDelete ? R.string.ConversationFragment_delete_everywhere : R.string.ConversationFragment_delete_for_everyone;
-
- if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && (!isNoteToSelfDelete || TextSecurePreferences.isMultiDevice(requireContext()))) {
- builder.setNeutralButton(deleteForEveryoneResId, (dialog, which) -> handleDeleteForEveryone(messageRecords));
- }
-
- builder.setNegativeButton(android.R.string.cancel, null);
- return builder;
- }
-
- private void onThreadDelete() {
- setThreadId(-1);
- conversationViewModel.clearThreadId();
- messageCountsViewModel.clearThreadId();
- listener.setThreadId(threadId);
- }
-
- private static boolean isNoteToSelfDelete(Set messageRecords) {
- return messageRecords.stream().allMatch(messageRecord -> messageRecord.isOutgoing() && messageRecord.getToRecipient().isSelf());
- }
-
- private void handleDeleteForEveryone(Set messageRecords) {
- Runnable deleteForEveryone = () -> {
- for (MessageRecord messageRecord : messageRecords) {
- listener.onRemoteDeleteMessage(messageRecord.getId());
- }
- SignalExecutors.BOUNDED.execute(() -> {
- for (MessageRecord message : messageRecords) {
- MessageSender.sendRemoteDelete(message.getId());
- }
- });
- };
-
- if (SignalStore.uiHints().hasConfirmedDeleteForEveryoneOnce() || isNoteToSelfDelete(messageRecords)) {
- deleteForEveryone.run();
- } else {
- new MaterialAlertDialogBuilder(requireActivity())
- .setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
- .setPositiveButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
- SignalStore.uiHints().markHasConfirmedDeleteForEveryoneOnce();
- deleteForEveryone.run();
- })
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
- }
-
- private void handleDisplayDetails(ConversationMessage message) {
- MessageDetailsFragment.create(message.getMessageRecord(), recipient.getId()).show(getParentFragment().getChildFragmentManager(), null);
- }
-
- private void handleForwardMessageParts(Set multiselectParts) {
- listener.onForwardClicked();
-
- MultiselectForwardFragmentArgs.create(requireContext(),
- multiselectParts,
- args -> MultiselectForwardFragment.showBottomSheet(getChildFragmentManager(),
- args));
- }
-
- private void handleResendMessage(final MessageRecord message) {
- final Context context = getActivity().getApplicationContext();
- new AsyncTask() {
- @Override
- protected Void doInBackground(MessageRecord... messageRecords) {
- MessageSender.resend(context, messageRecords[0]);
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
- }
-
- private void handleReplyMessage(final ConversationMessage message) {
- listener.handleReplyMessage(message);
- }
-
- private void handleEditMessage(@NonNull ConversationMessage selectedConversationMessage) {
- listener.handleEditMessage(selectedConversationMessage);
- }
-
- private void handleSaveAttachment(final MediaMmsMessageRecord message) {
- if (message.isViewOnce()) {
- throw new AssertionError("Cannot save a view-once message.");
- }
-
- SaveAttachmentTask.showWarningDialog(getActivity(), (dialog, which) -> {
- if (StorageUtil.canWriteToMediaStore()) {
- performSave(message);
- return;
- }
-
- Permissions.with(this)
- .request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- .ifNecessary()
- .withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
- .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
- .onAllGranted(() -> performSave(message))
- .execute();
- });
- }
-
- private void handleViewPaymentDetails(MessageRecord message) {
- if (message instanceof MediaMmsMessageRecord) {
- MediaMmsMessageRecord mediaMessage = (MediaMmsMessageRecord) message;
- if (mediaMessage.isPaymentNotification() && mediaMessage.getPayment() != null) {
- startActivity(PaymentsActivity.navigateToPaymentDetails(requireContext(), mediaMessage.getPayment().getUuid()));
- }
- }
- }
-
- private void performSave(final MediaMmsMessageRecord message) {
- List attachments = Stream.of(message.getSlideDeck().getSlides())
- .filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
- .map(s -> new SaveAttachmentTask.Attachment(s.getUri(), s.getContentType(), message.getDateSent(), s.getFileName().orElse(null)))
- .toList();
-
- if (!Util.isEmpty(attachments)) {
- SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
- saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, attachments.toArray(new SaveAttachmentTask.Attachment[0]));
- return;
- }
-
- Log.w(TAG, "No slide with attachable media found, failing nicely.");
- Toast.makeText(getActivity(),
- getResources().getQuantityString(R.plurals.ConversationFragment_error_while_saving_attachments_to_sd_card, 1),
- Toast.LENGTH_LONG).show();
- }
-
- public void stageOutgoingMessage(OutgoingMessage message) {
- if (message.getScheduledDate() != -1 || message.isMessageEdit()) {
- return;
- }
-
- if (getListAdapter() != null) {
- setLastSeen(0);
- list.post(() -> list.scrollToPosition(0));
- }
- }
-
- private void presentConversationMetadata(@NonNull ConversationData conversation) {
- if (conversationData != null && conversationData.getThreadId() == conversation.getThreadId()) {
- Log.d(TAG, "Already presented conversation data for thread " + threadId);
- return;
- }
-
- conversationData = conversation;
-
- ConversationAdapter adapter = getListAdapter();
- if (adapter == null) {
- return;
- }
-
- adapter.setFooterView(conversationHeader);
-
- Runnable afterScroll = () -> {
- if (!conversation.getMessageRequestData().isMessageRequestAccepted()) {
- snapToTopDataObserver.requestScrollPosition(adapter.getItemCount() - 1);
- }
-
- setLastSeen(conversation.getLastSeen());
-
- listener.onCursorChanged();
-
- conversationScrollListener.onScrolled(list, 0, 0);
- };
-
- int lastSeenPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastSeenPosition());
- int lastScrolledPosition = adapter.getAdapterPositionForMessagePosition(conversation.getLastScrolledPosition());
-
- if (conversation.getThreadSize() == 0) {
- afterScroll.run();
- } else if (conversation.shouldJumpToMessage()) {
- snapToTopDataObserver.buildScrollPosition(conversation.getJumpToPosition())
- .withOnScrollRequestComplete(() -> {
- afterScroll.run();
- getListAdapter().pulseAtPosition(conversation.getJumpToPosition());
- })
- .submit();
- } else if (conversation.getMessageRequestData().isMessageRequestAccepted()) {
- snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition)
- .withOnPerformScroll((layoutManager, position) -> scrollToLastSeenIfNecessary(conversation, layoutManager, position, 0))
- .withOnScrollRequestComplete(afterScroll)
- .submit();
- } else {
- snapToTopDataObserver.buildScrollPosition(adapter.getItemCount() - 1)
- .withOnScrollRequestComplete(afterScroll)
- .submit();
- }
- }
-
- private void scrollToLastSeenIfNecessary(ConversationData conversation, LinearLayoutManager layoutManager, int position, int count) {
- if (getView() == null) {
- Log.w(TAG, "[scrollToLastSeenIfNecessary] No view! Skipping.");
- return;
- }
-
- if (count < MAX_SCROLL_DELAY_COUNT && (list.getHeight() == 0 || lastSeenScrollOffset == 0)) {
- Log.w(TAG, "[scrollToLastSeenIfNecessary] List height or scroll offsets not available yet. Delaying jumping to last seen.");
- requireView().post(() -> scrollToLastSeenIfNecessary(conversation, layoutManager, position, count + 1));
- } else {
- if (count >= MAX_SCROLL_DELAY_COUNT) {
- Log.w(TAG, "[scrollToLastSeeenIfNecessary] Hit maximum call count! Doing default behavior.");
- }
-
- int offset = list.getHeight() - (conversation.shouldScrollToLastSeen() ? lastSeenScrollOffset : 0);
- layoutManager.scrollToPositionWithOffset(position, offset);
- }
- }
-
- private void updateNotificationProfileStatus(@NonNull Optional activeProfile) {
- if (activeProfile.isPresent() && activeProfile.get().getId() != SignalStore.notificationProfileValues().getLastProfilePopup()) {
- requireView().postDelayed(() -> {
- SignalStore.notificationProfileValues().setLastProfilePopup(activeProfile.get().getId());
- SignalStore.notificationProfileValues().setLastProfilePopupTime(System.currentTimeMillis());
- TopToastPopup.show(((ViewGroup) requireView()), R.drawable.ic_moon_16, getString(R.string.ConversationFragment__s_on, activeProfile.get().getName()));
- }, 500L);
- }
- }
-
- private boolean isAtBottom() {
- if (list.getChildCount() == 0) return true;
-
- int firstVisiblePosition = getListLayoutManager().findFirstVisibleItemPosition();
-
- if (isTypingIndicatorShowing()) {
- RecyclerView.ViewHolder item1 = list.findViewHolderForAdapterPosition(1);
- return firstVisiblePosition <= 1 && item1 != null && item1.itemView.getBottom() <= list.getHeight();
- }
-
- return firstVisiblePosition == 0 && list.getChildAt(0).getBottom() <= list.getHeight();
- }
-
- private boolean isTypingIndicatorShowing() {
- return getListAdapter().isTypingViewEnabled();
- }
-
- private void onSearchQueryUpdated(@Nullable String query) {
- if (getListAdapter() != null) {
- getListAdapter().onSearchQueryUpdated(query);
- }
- }
-
- public @NonNull Colorizer getColorizer() {
- return Objects.requireNonNull(colorizer);
- }
-
- @SuppressWarnings("CodeBlock2Expr")
- public void jumpToMessage(@NonNull RecipientId author, long timestamp, @Nullable Runnable onMessageNotFound) {
- SimpleTask.run(getLifecycle(), () -> {
- return SignalDatabase.messages().getMessagePositionInConversation(threadId, timestamp, author);
- }, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), onMessageNotFound));
- }
-
- private void moveToPosition(int position, @Nullable Runnable onMessageNotFound) {
- Log.d(TAG, "moveToPosition(" + position + ")");
- conversationViewModel.getPagingController().onDataNeededAroundIndex(position);
- snapToTopDataObserver.buildScrollPosition(position)
- .withOnPerformScroll(((layoutManager, p) ->
- list.post(() -> {
- if (Math.abs(layoutManager.findFirstVisibleItemPosition() - p) < SCROLL_ANIMATION_THRESHOLD) {
- View child = layoutManager.findViewByPosition(position);
-
- if (child == null || !layoutManager.isViewPartiallyVisible(child, true, false)) {
- layoutManager.scrollToPositionWithOffset(p, list.getHeight() / 4);
- }
- } else {
- layoutManager.scrollToPositionWithOffset(p, list.getHeight() / 4);
- }
- getListAdapter().pulseAtPosition(position);
- })
- ))
- .withOnInvalidPosition(() -> {
- if (onMessageNotFound != null) {
- onMessageNotFound.run();
- }
- Log.w(TAG, "[moveToMentionPosition] Tried to navigate to mention, but it wasn't found.");
- })
- .submit();
- }
-
- private void maybeShowSwipeToReplyTooltip() {
- if (!TextSecurePreferences.hasSeenSwipeToReplyTooltip(requireContext())) {
- int text = ViewUtil.isLtr(requireContext()) ? R.string.ConversationFragment_you_can_swipe_to_the_right_reply
- : R.string.ConversationFragment_you_can_swipe_to_the_left_reply;
- Snackbar.make(list, text, Snackbar.LENGTH_LONG).show();
-
- TextSecurePreferences.setHasSeenSwipeToReplyTooltip(requireContext(), true);
- }
- }
-
- private void scrollToNextMention() {
- SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
- return SignalDatabase.messages().getOldestUnreadMentionDetails(threadId);
- }, (pair) -> {
- if (pair != null) {
- jumpToMessage(pair.first(), pair.second(), () -> {});
- }
- });
- }
-
- private void postMarkAsReadRequest() {
- Optional timestamp = MarkReadHelper.getLatestTimestamp(Objects.requireNonNull(getListAdapter()), getListLayoutManager());
- timestamp.ifPresent(conversationViewModel::submitMarkReadRequest);
- }
-
- private void updateToolbarDependentMargins() {
- Toolbar toolbar = requireActivity().findViewById(R.id.toolbar);
- toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- Rect rect = new Rect();
- toolbar.getGlobalVisibleRect(rect);
- conversationViewModel.setToolbarBottom(rect.bottom);
- ViewUtil.setTopMargin(conversationHeader, rect.bottom + ViewUtil.dpToPx(16));
- toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
- }
- });
- }
-
- private @NonNull String calculateSelectedItemCount() {
- ConversationAdapter adapter = getListAdapter();
- int count = 0;
- if (adapter != null && !adapter.getSelectedItems().isEmpty()) {
- count = (int) adapter.getSelectedItems()
- .stream()
- .map(MultiselectPart::getConversationMessage)
- .distinct()
- .count();
- }
-
- return requireContext().getResources().getQuantityString(R.plurals.conversation_context__s_selected, count, count);
-
- }
-
- @Override
- public void onFinishForwardAction() {
- if (actionMode != null) {
- actionMode.finish();
- }
- }
-
- @Override
- public void onDismissForwardSheet() {
- }
-
- @Override
- public @Nullable Stories.MediaTransform.SendRequirements getStorySendRequirements() {
- return null;
- }
-
- @Override
- public @NonNull ItemClickListener getConversationAdapterListener() {
- return selectionClickListener;
- }
-
- @Override
- public void jumpToMessage(@NonNull MessageRecord messageRecord) {
- SimpleTask.run(getLifecycle(), () -> {
- return SignalDatabase.messages().getMessagePositionInConversation(threadId,
- messageRecord.getDateReceived(),
- messageRecord.getFromRecipient().getId());
- }, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> {
- Toast.makeText(getContext(), R.string.ConversationFragment_failed_to_open_message, Toast.LENGTH_SHORT).show();
- }));
- }
-
- public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
- int getSendButtonTint();
- boolean isKeyboardOpen();
- boolean isAttachmentKeyboardOpen();
- void openAttachmentKeyboard();
- void setThreadId(long threadId);
- void handleReplyMessage(ConversationMessage conversationMessage);
- void handleEditMessage(@NonNull ConversationMessage conversationMessage);
- void onMessageActionToolbarOpened();
- void onMessageActionToolbarClosed();
- void onBottomActionBarVisibilityChanged(int visibility);
- void onForwardClicked();
- void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
- void handleReaction(@NonNull ConversationMessage conversationMessage,
- @NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
- @NonNull SelectedConversationModel selectedConversationModel,
- @NonNull ConversationReactionOverlay.OnHideListener onHideListener);
- void onCursorChanged();
- void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
- void onFirstRender();
- void onVoiceNotePause(@NonNull Uri uri);
- void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress);
- void onVoiceNoteResume(@NonNull Uri uri, long messageId);
- void onVoiceNoteSeekTo(@NonNull Uri uri, double progress);
- void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
- void onRegisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver);
- void onUnregisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver);
- void onInviteToSignal();
- void onDeleteMessage(long id);
- void onRemoteDeleteMessage(long targetId);
- boolean isInBubble();
- }
-
- private class ConversationScrollListener extends OnScrollListener {
-
- private final ConversationDateHeader conversationDateHeader;
-
- private boolean wasAtBottom = true;
- private long lastPositionId = -1;
-
- ConversationScrollListener(@NonNull Context context) {
- this.conversationDateHeader = new ConversationDateHeader(context, scrollDateHeader);
-
- }
-
- @Override
- public void onScrolled(@NonNull final RecyclerView rv, final int dx, final int dy) {
- boolean currentlyAtBottom = !rv.canScrollVertically(1);
- boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
- int positionId = getHeaderPositionId();
-
- if (currentlyAtBottom && !wasAtBottom) {
- ViewUtil.fadeOut(composeDivider, 50, View.INVISIBLE);
- } else if (!currentlyAtBottom && wasAtBottom) {
- ViewUtil.fadeIn(composeDivider, 500);
- }
-
- if (currentlyAtBottom) {
- conversationViewModel.setShowScrollButtons(false);
- } else if (currentlyAtZoomScrollHeight) {
- conversationViewModel.setShowScrollButtons(true);
- }
-
- if (positionId != lastPositionId) {
- bindScrollHeader(conversationDateHeader, positionId);
- }
-
- wasAtBottom = currentlyAtBottom;
- lastPositionId = positionId;
-
- postMarkAsReadRequest();
- }
-
- @Override
- public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
- if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
- conversationDateHeader.show();
- } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- conversationDateHeader.hide();
- }
- }
-
- private boolean isAtZoomScrollHeight() {
- return getListLayoutManager().findFirstCompletelyVisibleItemPosition() > 4;
- }
-
- private int getHeaderPositionId() {
- return getListLayoutManager().findLastVisibleItemPosition();
- }
-
- private void bindScrollHeader(StickyHeaderViewHolder headerViewHolder, int positionId) {
- if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) {
- ((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId, ConversationAdapter.HEADER_TYPE_POPOVER_DATE);
- }
- }
- }
-
- private class ConversationFragmentItemClickListener implements ItemClickListener {
-
- @Override
- public void onItemClick(MultiselectPart item) {
- if (actionMode != null) {
- ((ConversationAdapter) list.getAdapter()).toggleSelection(item);
- list.invalidateItemDecorations();
-
- if (getListAdapter().getSelectedItems().size() == 0) {
- actionMode.finish();
- } else {
- setCorrectActionModeMenuVisibility();
- actionMode.setTitle(calculateSelectedItemCount());
- }
- }
- }
-
- @Override
- public void onItemLongClick(View itemView, MultiselectPart item) {
-
- if (actionMode != null) return;
-
- MessageRecord messageRecord = item.getConversationMessage().getMessageRecord();
-
- if (isUnopenedGift(itemView, messageRecord)) {
- return;
- }
-
- if (MessageRecordUtil.isValidReactionTarget(messageRecord) &&
- !recipient.get().isBlocked() &&
- !messageRequestViewModel.shouldShowMessageRequest() &&
- (!recipient.get().isGroup() || recipient.get().isActiveGroup()) &&
- ((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
- {
- multiselectItemDecoration.setFocusedItem(new MultiselectPart.Message(item.getConversationMessage()));
- list.invalidateItemDecorations();
-
- reactionsShade.setVisibility(View.VISIBLE);
- list.setLayoutFrozen(true);
-
- if (itemView instanceof ConversationItem) {
- Uri audioUri = getAudioUriForLongClick(messageRecord);
- if (audioUri != null) {
- listener.onVoiceNotePause(audioUri);
- }
-
- Bitmap videoBitmap = null;
- int childAdapterPosition = list.getChildAdapterPosition(itemView);
-
- GiphyMp4ProjectionPlayerHolder mp4Holder = null;
- if (childAdapterPosition != RecyclerView.NO_POSITION) {
- mp4Holder = giphyMp4ProjectionRecycler.getCurrentHolder(childAdapterPosition);
- if (mp4Holder != null && mp4Holder.isVisible()) {
- mp4Holder.pause();
- videoBitmap = mp4Holder.getBitmap();
- mp4Holder.hide();
- } else {
- mp4Holder = null;
- }
- }
- final GiphyMp4ProjectionPlayerHolder finalMp4Holder = mp4Holder;
-
- ConversationItem conversationItem = (ConversationItem) itemView;
- Bitmap bitmap = ConversationItemSelection.snapshotView(conversationItem, list, messageRecord, videoBitmap);
-
- View focusedView = listener.isKeyboardOpen() ? conversationItem.getRootView().findFocus() : null;
-
- final ConversationItemBodyBubble bodyBubble = conversationItem.bodyBubble;
- SelectedConversationModel selectedConversationModel = new SelectedConversationModel(bitmap,
- itemView.getX(),
- itemView.getY() + list.getTranslationY(),
- bodyBubble.getX(),
- bodyBubble.getY(),
- bodyBubble.getWidth(),
- audioUri,
- messageRecord.isOutgoing(),
- focusedView);
-
- bodyBubble.setVisibility(View.INVISIBLE);
- conversationItem.reactionsView.setVisibility(View.INVISIBLE);
-
- boolean quotedIndicatorVisible = conversationItem.quotedIndicator != null && conversationItem.quotedIndicator.getVisibility() == View.VISIBLE;
- if (quotedIndicatorVisible && conversationItem.quotedIndicator != null) {
- ViewUtil.fadeOut(conversationItem.quotedIndicator, 150, View.INVISIBLE);
- }
-
- ViewUtil.hideKeyboard(requireContext(), conversationItem);
-
- boolean showScrollButtons = conversationViewModel.getShowScrollButtons();
- if (showScrollButtons) {
- conversationViewModel.setShowScrollButtons(false);
- }
-
- boolean isAttachmentKeyboardOpen = listener.isAttachmentKeyboardOpen();
-
- listener.handleReaction(item.getConversationMessage(),
- new ReactionsToolbarListener(item.getConversationMessage()),
- selectedConversationModel,
- new ConversationReactionOverlay.OnHideListener() {
- @Override public void startHide() {
- multiselectItemDecoration.hideShade(list);
- ViewUtil.fadeOut(reactionsShade, getResources().getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE);
- }
-
- @Override public void onHide() {
- list.setLayoutFrozen(false);
-
- if (selectedConversationModel.getAudioUri() != null) {
- listener.onVoiceNoteResume(selectedConversationModel.getAudioUri(), messageRecord.getId());
- }
-
- if (getActivity() != null) {
- WindowUtil.setLightStatusBarFromTheme(requireActivity());
- WindowUtil.setLightNavigationBarFromTheme(requireActivity());
- }
-
- clearFocusedItem();
-
- if (finalMp4Holder != null) {
- finalMp4Holder.show();
- finalMp4Holder.resume();
- }
-
- bodyBubble.setVisibility(View.VISIBLE);
- conversationItem.reactionsView.setVisibility(View.VISIBLE);
- if (quotedIndicatorVisible && conversationItem.quotedIndicator != null) {
- ViewUtil.fadeIn(conversationItem.quotedIndicator, 150);
- }
-
- if (showScrollButtons) {
- conversationViewModel.setShowScrollButtons(true);
- }
-
- if (isAttachmentKeyboardOpen) {
- listener.openAttachmentKeyboard();
- }
- }
- });
- }
- } else {
- clearFocusedItem();
- ((ConversationAdapter) list.getAdapter()).toggleSelection(item);
- list.invalidateItemDecorations();
-
- actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
- }
- }
-
- @Nullable private Uri getAudioUriForLongClick(@NonNull MessageRecord messageRecord) {
- VoiceNotePlaybackState playbackState = listener.getVoiceNoteMediaController().getVoiceNotePlaybackState().getValue();
- if (playbackState == null || !playbackState.isPlaying()) {
- return null;
- }
-
- if (!MessageRecordUtil.hasAudio(messageRecord) || !messageRecord.isMms()) {
- return null;
- }
-
- Uri messageUri = ((MmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide().getUri();
- return playbackState.getUri().equals(messageUri) ? messageUri : null;
- }
-
- @Override
- public void onQuoteClicked(MmsMessageRecord messageRecord) {
- if (messageRecord.getQuote() == null) {
- Log.w(TAG, "Received a 'quote clicked' event, but there's no quote...");
- return;
- }
-
- if (messageRecord.getQuote().isOriginalMissing()) {
- Log.i(TAG, "Clicked on a quote whose original message we never had.");
- Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show();
- return;
- }
-
- if (messageRecord.getParentStoryId() != null) {
- startActivity(StoryViewerActivity.createIntent(
- requireContext(),
- new StoryViewerArgs.Builder(messageRecord.getQuote().getAuthor(), Recipient.resolved(messageRecord.getQuote().getAuthor()).shouldHideStory())
- .withStoryId(messageRecord.getParentStoryId().asMessageId().getId())
- .isFromQuote(true)
- .build()));
- return;
- }
-
- SimpleTask.run(getLifecycle(), () -> {
- return SignalDatabase.messages().getQuotedMessagePosition(threadId,
- messageRecord.getQuote().getId(),
- messageRecord.getQuote().getAuthor());
- }, p -> moveToPosition(p + (isTypingIndicatorShowing() ? 1 : 0), () -> {
- Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_no_longer_available, Toast.LENGTH_SHORT).show();
- }));
- }
-
- @Override
- public void onLinkPreviewClicked(@NonNull LinkPreview linkPreview) {
- if (getContext() != null && getActivity() != null) {
- CommunicationActions.openBrowserLink(getActivity(), linkPreview.getUrl());
- }
- }
-
- @Override
- public void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord) {
- if (getContext() != null && getActivity() != null) {
- MessageQuotesBottomSheet.show(
- getChildFragmentManager(),
- new MessageId(messageRecord.getId()),
- recipient.getId()
- );
- }
- }
-
- @Override
- public void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms) {
- if (getContext() != null && getActivity() != null) {
- LongMessageFragment.create(messageId, isMms).show(getChildFragmentManager(), null);
- }
- }
-
- @Override
- public void onStickerClicked(@NonNull StickerLocator sticker) {
- if (getContext() != null && getActivity() != null) {
- startActivity(StickerPackPreviewActivity.getIntent(sticker.getPackId(), sticker.getPackKey()));
- }
- }
-
- @Override
- public void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord) {
- if (!messageRecord.isViewOnce()) {
- throw new AssertionError("Non-revealable message clicked.");
- }
-
- if (!ViewOnceUtil.isViewable(messageRecord)) {
- int stringRes = messageRecord.isOutgoing() ? R.string.ConversationFragment_outgoing_view_once_media_files_are_automatically_removed
- : R.string.ConversationFragment_you_already_viewed_this_message;
- Toast.makeText(requireContext(), stringRes, Toast.LENGTH_SHORT).show();
- return;
- }
-
- SimpleTask.run(getLifecycle(), () -> {
- Log.i(TAG, "Copying the view-once photo to temp storage and deleting underlying media.");
-
- try {
- Slide thumbnailSlide = messageRecord.getSlideDeck().getThumbnailSlide();
- InputStream inputStream = PartAuthority.getAttachmentStream(requireContext(), thumbnailSlide.getUri());
- Uri tempUri = BlobProvider.getInstance().forData(inputStream, thumbnailSlide.getFileSize())
- .withMimeType(thumbnailSlide.getContentType())
- .createForSingleSessionOnDisk(requireContext());
-
- SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(messageRecord.getId());
-
- ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
-
- ApplicationDependencies.getJobManager().add(new MultiDeviceViewOnceOpenJob(new MessageTable.SyncMessageId(messageRecord.getFromRecipient().getId(), messageRecord.getDateSent())));
-
- return tempUri;
- } catch (IOException e) {
- return null;
- }
- }, (uri) -> {
- if (uri != null) {
- startActivity(ViewOnceMessageActivity.getIntent(requireContext(), messageRecord.getId(), uri));
- } else {
- Log.w(TAG, "Failed to open view-once photo. Showing a toast and deleting the attachments for the message just in case.");
- Toast.makeText(requireContext(), R.string.ConversationFragment_failed_to_open_message, Toast.LENGTH_SHORT).show();
- SignalExecutors.BOUNDED.execute(() -> SignalDatabase.attachments().deleteAttachmentFilesForViewOnceMessage(messageRecord.getId()));
- }
- });
- }
-
- @Override
- public void onSharedContactDetailsClicked(@NonNull Contact contact, @NonNull View avatarTransitionView) {
- if (getContext() != null && getActivity() != null) {
- ViewCompat.setTransitionName(avatarTransitionView, "avatar");
- Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), avatarTransitionView, "avatar").toBundle();
- ActivityCompat.startActivity(getActivity(), SharedContactDetailsActivity.getIntent(getContext(), contact), bundle);
- }
- }
-
- @Override
- public void onAddToContactsClicked(@NonNull Contact contactWithAvatar) {
- if (getContext() == null) {
- return;
- }
-
- disposables.add(AddToContactsContract.createIntentAndLaunch(
- ConversationFragment.this,
- addToContactsLauncher,
- contactWithAvatar
- ));
- }
-
- @Override
- public void onMessageSharedContactClicked(@NonNull List choices) {
- if (getContext() == null) return;
-
- ContactUtil.selectRecipientThroughDialog(getContext(), choices, locale, recipient -> {
- CommunicationActions.startConversation(getContext(), recipient, null);
- });
- }
-
- @Override
- public void onInviteSharedContactClicked(@NonNull List choices) {
- if (getContext() == null) return;
-
- ContactUtil.selectRecipientThroughDialog(getContext(), choices, locale, recipient -> {
- CommunicationActions.composeSmsThroughDefaultApp(getContext(), recipient, getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
- });
- }
-
- @Override
- public void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms) {
- if (getParentFragment() == null) return;
- final String REACTIONS_TAG = "REACTIONS";
-
- if (getParentFragmentManager().findFragmentByTag(REACTIONS_TAG) == null) {
- ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(getParentFragmentManager(), REACTIONS_TAG);
- }
- }
-
- @Override
- public void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
- if (getParentFragment() == null) return;
-
- RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(getParentFragmentManager(), "BOTTOM");
- }
-
- @Override
- public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
- listener.onMessageWithErrorClicked(messageRecord);
- }
-
- @Override
- public void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord) {
- RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
- }
-
- @Override
- public void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId) {
- SafetyNumberBottomSheet.forRecipientId(recipientId)
- .show(getParentFragmentManager());
- }
-
- @Override
- public void onVoiceNotePause(@NonNull Uri uri) {
- listener.onVoiceNotePause(uri);
- }
-
- @Override
- public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
- listener.onVoiceNotePlay(uri, messageId, progress);
- }
-
- @Override
- public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
- listener.onVoiceNoteSeekTo(uri, progress);
- }
-
- @Override
- public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
- listener.onVoiceNotePlaybackSpeedChanged(uri, speed);
- }
-
- @Override
- public void onRegisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver) {
- listener.onRegisterVoiceNoteCallbacks(onPlaybackStartObserver);
- }
-
- @Override
- public void onUnregisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver) {
- listener.onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver);
- }
-
- @Override
- public boolean onUrlClicked(@NonNull String url) {
- return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) ||
- CommunicationActions.handlePotentialProxyLinkUrl(requireActivity(), url);
- }
-
- @Override
- public void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange) {
- if (getParentFragment() == null) {
- return;
- }
-
- GroupsV1MigrationInfoBottomSheetDialogFragment.show(getParentFragmentManager(), membershipChange);
- }
-
- @Override
- public void onChatSessionRefreshLearnMoreClicked() {
- ConversationDialogs.INSTANCE.displayChatSessionRefreshLearnMoreDialog(requireContext());
- }
-
- @Override
- public void onBadDecryptLearnMoreClicked(@NonNull RecipientId author) {
- SimpleTask.run(getLifecycle(),
- () -> Recipient.resolved(author).getDisplayName(requireContext()),
- name -> BadDecryptLearnMoreDialog.show(getParentFragmentManager(), name, recipient.get().isGroup()));
- }
-
- @Override
- public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
- ConversationDialogs.INSTANCE.displaySafetyNumberLearnMoreDialog(ConversationFragment.this, recipient);
- }
- @Override
- public void onJoinGroupCallClicked() {
- CommunicationActions.startVideoCall(requireActivity(), recipient.get());
- }
-
- @Override
- public void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId) {
- GroupLinkInviteFriendsBottomSheetDialogFragment.show(requireActivity().getSupportFragmentManager(), groupId);
- }
-
- @Override
- public void onEnableCallNotificationsClicked() {
- EnableCallNotificationSettingsDialog.fixAutomatically(requireContext());
- if (EnableCallNotificationSettingsDialog.shouldShow(requireContext())) {
- EnableCallNotificationSettingsDialog.show(getChildFragmentManager());
- } else {
- refreshList();
- }
- }
-
- @Override
- public void onPlayInlineContent(ConversationMessage conversationMessage) {
- getListAdapter().playInlineContent(conversationMessage);
- }
-
- @Override
- public void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord) {
- ConversationDialogs.INSTANCE.displayInMemoryMessageDialog(requireContext(), messageRecord);
- }
-
- @Override
- public void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted) {
- if (groupId != null) {
- GroupDescriptionDialog.show(getChildFragmentManager(), groupId, description, isMessageRequestAccepted);
- }
- }
-
- @Override
- public void onChangeNumberUpdateContact(@NonNull Recipient recipient) {
- startActivity(RecipientExporter.export(recipient).asAddContactIntent());
- }
-
- @Override
- public void onCallToAction(@NonNull String action) {
- if ("gift_badge".equals(action)) {
- startActivity(new Intent(requireContext(), GiftFlowActivity.class));
- }
- }
-
- @Override
- public void onDonateClicked() {
- requireActivity().getSupportFragmentManager()
- .beginTransaction()
- .add(DonateToSignalFragment.Dialog.create(DonateToSignalType.ONE_TIME), "one_time_nav")
- .commitNow();
- }
-
- @Override
- public void onBlockJoinRequest(@NonNull Recipient recipient) {
- new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.ConversationFragment__block_request)
- .setMessage(getString(R.string.ConversationFragment__s_will_not_be_able_to_join_or_request_to_join_this_group_via_the_group_link, recipient.getDisplayName(requireContext())))
- .setNegativeButton(R.string.ConversationFragment__cancel, null)
- .setPositiveButton(R.string.ConversationFragment__block_request_button, (d, w) -> handleBlockJoinRequest(recipient))
- .show();
- }
-
- @Override
- public void onRecipientNameClicked(@NonNull RecipientId target) {
- if (getParentFragment() == null) return;
-
- RecipientBottomSheetDialogFragment.create(target, recipient.get().getGroupId().orElse(null)).show(getParentFragmentManager(), "BOTTOM");
- }
-
- @Override
- public void onInviteToSignalClicked() {
- listener.onInviteToSignal();
- }
-
- @Override
- public void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord) {
- if (!MessageRecordUtil.hasGiftBadge(messageRecord)) {
- return;
- }
-
- if (messageRecord.isOutgoing()) {
- ViewSentGiftBottomSheet.show(getChildFragmentManager(), (MmsMessageRecord) messageRecord);
- } else {
- ViewReceivedGiftBottomSheet.show(getChildFragmentManager(), (MmsMessageRecord) messageRecord);
- }
- }
-
- @Override
- public void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord) {
- if (messageRecord.isOutgoing() && MessageRecordUtil.hasGiftBadge(messageRecord)) {
- conversationViewModel.markGiftBadgeRevealed(messageRecord.getId());
- }
- }
-
- @Override
- public void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args) {
- if (listener.isInBubble()) {
- Intent intent = ConversationIntents.createBuilderSync(requireActivity(), recipient.getId(), threadId)
- .withStartingPosition(list.getChildAdapterPosition(parent))
- .build();
-
- requireActivity().startActivity(intent);
- requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args.skipSharedElementTransition(true)));
- return;
- }
-
- if (args.isVideoGif()) {
- int adapterPosition = list.getChildAdapterPosition(parent);
- GiphyMp4ProjectionPlayerHolder holder = giphyMp4ProjectionRecycler.getCurrentHolder(adapterPosition);
-
- if (holder != null) {
- parent.showProjectionArea();
- holder.hide();
- }
- }
-
- sharedElement.setTransitionName(MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
- requireActivity().setExitSharedElementCallback(new MaterialContainerTransformSharedElementCallback());
- ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), sharedElement, MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
-
- requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args), options.toBundle());
- }
-
- @Override
- public void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord) {
- if (messageRecord.isOutgoing()) {
- EditMessageHistoryDialog.show(getChildFragmentManager(), messageRecord.getToRecipient().getId(), messageRecord);
- } else {
- EditMessageHistoryDialog.show(getChildFragmentManager(), messageRecord.getFromRecipient().getId(), messageRecord);
- }
- }
-
- @Override
- public void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks) {
- GroupDescriptionDialog.show(getChildFragmentManager(), groupName, description, shouldLinkifyWebLinks);
- }
-
- @Override
- public void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey) {
- CommunicationActions.startVideoCall(ConversationFragment.this, callLinkRootKey);
- }
-
- @Override
- public void onActivatePaymentsClicked() {
- Intent intent = new Intent(requireContext(), PaymentsActivity.class);
- startActivity(intent);
- }
-
- @Override
- public void onSendPaymentClicked(@NonNull RecipientId recipientId) {
- AttachmentManager.selectPayment(ConversationFragment.this, recipient.get());
- }
-
- @Override
- public void onScheduledIndicatorClicked(@NonNull View view, @NonNull ConversationMessage conversationMessage) {
-
- }
- }
-
- private boolean isUnopenedGift(View itemView, MessageRecord messageRecord) {
- if (itemView instanceof OpenableGift) {
- Projection projection = ((OpenableGift) itemView).getOpenableGiftProjection(false);
- if (projection != null) {
- projection.release();
- return !openableGiftItemDecoration.hasOpenedGiftThisSession(messageRecord.getId());
- }
- }
-
- return false;
- }
-
- public void refreshList() {
- ConversationAdapter listAdapter = getListAdapter();
- if (listAdapter != null) {
- listAdapter.notifyDataSetChanged();
- }
- }
-
- private void handleEnterMultiSelect(@NonNull ConversationMessage conversationMessage) {
- Set multiselectParts = conversationMessage.getMultiselectCollection().toSet();
-
- multiselectParts.stream().forEach(part -> {
- ((ConversationAdapter) list.getAdapter()).toggleSelection(part);
- });
-
- list.invalidateItemDecorations();
-
- actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
- }
-
- private void handleBlockJoinRequest(@NonNull Recipient recipient) {
- disposables.add(
- groupViewModel.blockJoinRequests(ConversationFragment.this.recipient.get(), recipient)
- .subscribe(result -> {
- if (result.isFailure()) {
- int failureReason = GroupErrors.getUserDisplayMessage(((GroupBlockJoinRequestResult.Failure) result).getReason());
- Toast.makeText(requireContext(), failureReason, Toast.LENGTH_SHORT).show();
- } else {
- Toast.makeText(requireContext(), R.string.ConversationFragment__blocked, Toast.LENGTH_SHORT).show();
- }
- })
- );
- }
-
- private final class CheckExpirationDataObserver extends RecyclerView.AdapterDataObserver {
- @Override
- public void onItemRangeRemoved(int positionStart, int itemCount) {
- ConversationAdapter adapter = getListAdapter();
- if (adapter == null || actionMode == null) {
- return;
- }
-
- Set selected = adapter.getSelectedItems();
- Set expired = new HashSet<>();
-
- for (final MultiselectPart multiselectPart : selected) {
- if (multiselectPart.isExpired()) {
- expired.add(multiselectPart);
- }
- }
-
- adapter.removeFromSelection(expired);
-
- if (adapter.getSelectedItems().isEmpty()) {
- actionMode.finish();
- } else {
- actionMode.setTitle(calculateSelectedItemCount());
- }
- }
- }
-
- private final class ConversationSnapToTopDataObserver extends SnapToTopDataObserver {
-
- public ConversationSnapToTopDataObserver(@NonNull RecyclerView recyclerView,
- @Nullable ScrollRequestValidator scrollRequestValidator)
- {
- super(recyclerView, scrollRequestValidator, () -> {
- list.scrollToPosition(0);
- list.post(ConversationFragment.this::postMarkAsReadRequest);
- });
- }
-
- @Override
- public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
- // Do nothing.
- }
-
- @Override
- public void onItemRangeInserted(int positionStart, int itemCount) {
- if (positionStart == 0 && itemCount == 1 && isTypingIndicatorShowing()) {
- return;
- }
-
- super.onItemRangeInserted(positionStart, itemCount);
- }
-
- @Override
- public void onItemRangeChanged(int positionStart, int itemCount) {
- super.onItemRangeChanged(positionStart, itemCount);
- list.post(ConversationFragment.this::postMarkAsReadRequest);
- }
- }
-
- private final class ConversationScrollRequestValidator implements SnapToTopDataObserver.ScrollRequestValidator {
-
- @Override
- public boolean isPositionStillValid(int position) {
- if (getListAdapter() == null) {
- return position >= 0;
- } else {
- return position >= 0 && position < getListAdapter().getItemCount();
- }
- }
-
- @Override
- public boolean isItemAtPositionLoaded(int position) {
- if (getListAdapter() == null) {
- return false;
- } else if (getListAdapter().hasFooter() && position == getListAdapter().getItemCount() - 1) {
- return true;
- } else {
- return getListAdapter().getItem(position) != null;
- }
- }
- }
-
- private class ReactionsToolbarListener implements ConversationReactionOverlay.OnActionSelectedListener {
-
- private final ConversationMessage conversationMessage;
-
- private ReactionsToolbarListener(@NonNull ConversationMessage conversationMessage) {
- this.conversationMessage = conversationMessage;
- }
-
- @Override
- public void onActionSelected(@NonNull ConversationReactionOverlay.Action action) {
- switch (action) {
- case REPLY:
- handleReplyMessage(conversationMessage);
- break;
- case EDIT:
- handleEditMessage(conversationMessage);
- break;
- case FORWARD:
- handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet());
- break;
- case RESEND:
- handleResendMessage(conversationMessage.getMessageRecord());
- break;
- case DOWNLOAD:
- handleSaveAttachment((MediaMmsMessageRecord) conversationMessage.getMessageRecord());
- break;
- case COPY:
- handleCopyMessage(conversationMessage.getMultiselectCollection().toSet());
- break;
- case PAYMENT_DETAILS:
- handleViewPaymentDetails(conversationMessage.getMessageRecord());
- break;
- case MULTISELECT:
- handleEnterMultiSelect(conversationMessage);
- break;
- case VIEW_INFO:
- handleDisplayDetails(conversationMessage);
- break;
- case DELETE:
- handleDeleteMessages(conversationMessage.getMultiselectCollection().toSet());
- break;
- }
- }
- }
-
- private class ActionModeCallback implements ActionMode.Callback {
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- mode.setTitle(calculateSelectedItemCount());
-
- setCorrectActionModeMenuVisibility();
- listener.onMessageActionToolbarOpened();
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
- return false;
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- ((ConversationAdapter)list.getAdapter()).clearSelection();
- list.invalidateItemDecorations();
- setBottomActionBarVisibility(false);
- actionMode = null;
- listener.onMessageActionToolbarClosed();
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- return false;
- }
- }
-
- private static class ConversationDateHeader extends StickyHeaderViewHolder {
-
- private final Animation animateIn;
- private final Animation animateOut;
-
- private boolean pendingHide = false;
-
- private ConversationDateHeader(Context context, TextView textView) {
- super(textView);
- this.animateIn = AnimationUtils.loadAnimation(context, R.anim.slide_from_top);
- this.animateOut = AnimationUtils.loadAnimation(context, R.anim.slide_to_top);
-
- this.animateIn.setDuration(100);
- this.animateOut.setDuration(100);
- }
-
- public void show() {
- if (textView.getText() == null || textView.getText().length() == 0) {
- return;
- }
-
- if (pendingHide) {
- pendingHide = false;
- } else {
- ViewUtil.animateIn(textView, animateIn);
- }
- }
-
- public void hide() {
- pendingHide = true;
-
- textView.postDelayed(new Runnable() {
- @Override
- public void run() {
- if (pendingHide) {
- pendingHide = false;
- ViewUtil.animateOut(textView, animateOut, View.GONE);
- }
- }
- }, 400);
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java
deleted file mode 100644
index 0c6182e98a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java
+++ /dev/null
@@ -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 liveRecipient;
- private final LiveData groupActiveState;
- private final LiveData selfMembershipLevel;
- private final LiveData actionableRequestingMembers;
- private final LiveData reviewState;
- private final LiveData> gv1MigrationSuggestions;
- private final GroupManagementRepository groupManagementRepository;
-
- private boolean firstTimeInviteFriendsTriggered;
-
- private ConversationGroupViewModel() {
- this.liveRecipient = new MutableLiveData<>();
- this.groupManagementRepository = new GroupManagementRepository();
-
- LiveData groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient);
- LiveData> 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 getActionableRequestingMembers() {
- return actionableRequestingMembers;
- }
-
- LiveData getGroupActiveState() {
- return groupActiveState;
- }
-
- LiveData getSelfMemberLevel() {
- return selfMembershipLevel;
- }
-
- public LiveData getReviewState() {
- return reviewState;
- }
-
- @NonNull LiveData> 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 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 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 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 create(@NonNull Class modelClass) {
- //noinspection ConstantConditions
- return modelClass.cast(new ConversationGroupViewModel());
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
index 59d61f06e2..01faf3f1c5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java
@@ -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;
@@ -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;
- }
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java
deleted file mode 100644
index 3b55dfbdf6..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java
+++ /dev/null
@@ -1,4484 +0,0 @@
-/*
- * Copyright 2023 Signal Messenger, LLC
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-package org.thoughtcrime.securesms.conversation;
-
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.app.PendingIntent;
-import android.content.ActivityNotFoundException;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
-import android.content.res.ColorStateList;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Vibrator;
-import android.provider.Browser;
-import android.provider.ContactsContract;
-import android.provider.Settings;
-import android.text.Editable;
-import android.text.SpannableStringBuilder;
-import android.text.TextWatcher;
-import android.text.method.LinkMovementMethod;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnFocusChangeListener;
-import android.view.View.OnKeyListener;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.activity.OnBackPressedCallback;
-import androidx.annotation.ColorInt;
-import androidx.annotation.ColorRes;
-import androidx.annotation.IdRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.WorkerThread;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.SearchView;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.app.ActivityCompat;
-import androidx.core.content.ContextCompat;
-import androidx.core.content.pm.ShortcutInfoCompat;
-import androidx.core.content.pm.ShortcutManagerCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.core.graphics.drawable.IconCompat;
-import androidx.core.view.MenuItemCompat;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.airbnb.lottie.SimpleColorFilter;
-import com.annimon.stream.Collectors;
-import com.annimon.stream.Stream;
-import com.bumptech.glide.load.engine.DiskCacheStrategy;
-import com.bumptech.glide.request.target.CustomTarget;
-import com.bumptech.glide.request.transition.Transition;
-import com.google.android.material.button.MaterialButton;
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.signal.core.util.PendingIntentFlags;
-import org.signal.core.util.StringUtil;
-import org.signal.core.util.ThreadUtil;
-import org.signal.core.util.concurrent.LifecycleDisposable;
-import org.signal.core.util.concurrent.SignalExecutors;
-import org.signal.core.util.concurrent.SimpleTask;
-import org.signal.core.util.logging.Log;
-import org.signal.libsignal.protocol.InvalidMessageException;
-import org.signal.libsignal.protocol.util.Pair;
-import org.thoughtcrime.securesms.BlockUnblockDialog;
-import org.thoughtcrime.securesms.GroupMembersDialog;
-import org.thoughtcrime.securesms.MainActivity;
-import org.thoughtcrime.securesms.MuteDialog;
-import org.thoughtcrime.securesms.PromptMmsActivity;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.ShortcutLauncherActivity;
-import org.thoughtcrime.securesms.attachments.Attachment;
-import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
-import org.thoughtcrime.securesms.audio.AudioRecorder;
-import org.thoughtcrime.securesms.badges.gifts.thanks.GiftThanksSheet;
-import org.thoughtcrime.securesms.components.AnimatingToggle;
-import org.thoughtcrime.securesms.components.ComposeText;
-import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
-import org.thoughtcrime.securesms.components.HidingLinearLayout;
-import org.thoughtcrime.securesms.components.InputAwareLayout;
-import org.thoughtcrime.securesms.components.InputPanel;
-import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
-import org.thoughtcrime.securesms.components.SendButton;
-import org.thoughtcrime.securesms.components.TooltipPopup;
-import org.thoughtcrime.securesms.components.TypingStatusSender;
-import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
-import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
-import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
-import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
-import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
-import org.thoughtcrime.securesms.components.location.SignalPlace;
-import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
-import org.thoughtcrime.securesms.components.reminder.BubbleOptOutReminder;
-import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
-import org.thoughtcrime.securesms.components.reminder.GroupsV1MigrationSuggestionsReminder;
-import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsReminder;
-import org.thoughtcrime.securesms.components.reminder.Reminder;
-import org.thoughtcrime.securesms.components.reminder.ReminderView;
-import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
-import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
-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.VoiceNoteMediaController;
-import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
-import org.thoughtcrime.securesms.components.voice.VoiceNotePlayerView;
-import org.thoughtcrime.securesms.contacts.ContactAccessor;
-import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
-import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
-import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
-import org.thoughtcrime.securesms.contactshare.Contact;
-import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
-import org.thoughtcrime.securesms.contactshare.ContactUtil;
-import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
-import org.thoughtcrime.securesms.conversation.drafts.DraftViewModel;
-import org.thoughtcrime.securesms.conversation.ui.groupcall.GroupCallViewModel;
-import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery;
-import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener;
-import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsController;
-import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModel;
-import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
-import org.thoughtcrime.securesms.conversation.v2.ConversationDialogs;
-import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
-import org.thoughtcrime.securesms.crypto.SecurityEvent;
-import org.thoughtcrime.securesms.database.DraftTable.Draft;
-import org.thoughtcrime.securesms.database.DraftTable.Drafts;
-import org.thoughtcrime.securesms.database.GroupTable;
-import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus;
-import org.thoughtcrime.securesms.database.RecipientTable;
-import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState;
-import org.thoughtcrime.securesms.database.SignalDatabase;
-import org.thoughtcrime.securesms.database.ThreadTable;
-import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
-import org.thoughtcrime.securesms.database.model.GroupRecord;
-import org.thoughtcrime.securesms.database.model.IdentityRecord;
-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.MmsMessageRecord;
-import org.thoughtcrime.securesms.database.model.ReactionRecord;
-import org.thoughtcrime.securesms.database.model.StickerRecord;
-import org.thoughtcrime.securesms.database.model.StoryType;
-import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
-import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
-import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity;
-import org.thoughtcrime.securesms.groups.GroupId;
-import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
-import org.thoughtcrime.securesms.groups.ui.GroupErrors;
-import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
-import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
-import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog;
-import org.thoughtcrime.securesms.invites.InviteActions;
-import org.thoughtcrime.securesms.jobs.ForceUpdateGroupV2Job;
-import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
-import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
-import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
-import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
-import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
-import org.thoughtcrime.securesms.keyboard.KeyboardPage;
-import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
-import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment;
-import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
-import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment;
-import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment;
-import org.thoughtcrime.securesms.keyboard.sticker.StickerSearchDialogFragment;
-import org.thoughtcrime.securesms.keyvalue.PaymentsValues;
-import org.thoughtcrime.securesms.keyvalue.SignalStore;
-import org.thoughtcrime.securesms.linkpreview.LinkPreview;
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
-import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
-import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
-import org.thoughtcrime.securesms.maps.PlacePickerActivity;
-import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
-import org.thoughtcrime.securesms.mediasend.Media;
-import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
-import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
-import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
-import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
-import org.thoughtcrime.securesms.mms.AttachmentManager;
-import org.thoughtcrime.securesms.mms.AudioSlide;
-import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
-import org.thoughtcrime.securesms.mms.GifSlide;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.thoughtcrime.securesms.mms.ImageSlide;
-import org.thoughtcrime.securesms.mms.MediaConstraints;
-import org.thoughtcrime.securesms.mms.OutgoingMessage;
-import org.thoughtcrime.securesms.mms.QuoteModel;
-import org.thoughtcrime.securesms.mms.Slide;
-import org.thoughtcrime.securesms.mms.SlideDeck;
-import org.thoughtcrime.securesms.mms.SlideFactory.MediaType;
-import org.thoughtcrime.securesms.mms.StickerSlide;
-import org.thoughtcrime.securesms.mms.VideoSlide;
-import org.thoughtcrime.securesms.notifications.NotificationChannels;
-import org.thoughtcrime.securesms.notifications.v2.ConversationId;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.profiles.spoofing.ReviewBannerView;
-import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment;
-import org.thoughtcrime.securesms.providers.BlobProvider;
-import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment;
-import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
-import org.thoughtcrime.securesms.recipients.LiveRecipient;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.recipients.RecipientExporter;
-import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
-import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.recipients.RecipientUtil;
-import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
-import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
-import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
-import org.thoughtcrime.securesms.search.MessageResult;
-import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.thoughtcrime.securesms.sms.MessageSender;
-import org.thoughtcrime.securesms.sms.MessageSender.SendType;
-import org.thoughtcrime.securesms.stickers.StickerEventListener;
-import org.thoughtcrime.securesms.stickers.StickerLocator;
-import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
-import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
-import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
-import org.thoughtcrime.securesms.stories.StoryViewerArgs;
-import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity;
-import org.thoughtcrime.securesms.util.AsynchronousCallback;
-import org.thoughtcrime.securesms.util.BitmapUtil;
-import org.thoughtcrime.securesms.util.BubbleUtil;
-import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
-import org.thoughtcrime.securesms.util.CommunicationActions;
-import org.thoughtcrime.securesms.util.ContextUtil;
-import org.thoughtcrime.securesms.util.ConversationUtil;
-import org.thoughtcrime.securesms.util.Debouncer;
-import org.thoughtcrime.securesms.util.Dialogs;
-import org.thoughtcrime.securesms.util.DrawableUtil;
-import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.FullscreenHelper;
-import org.thoughtcrime.securesms.util.IdentityUtil;
-import org.thoughtcrime.securesms.util.Material3OnScrollHelper;
-import org.thoughtcrime.securesms.util.MediaUtil;
-import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
-import org.thoughtcrime.securesms.util.MessageRecordUtil;
-import org.thoughtcrime.securesms.util.MessageUtil;
-import org.thoughtcrime.securesms.util.PlayStoreUtil;
-import org.thoughtcrime.securesms.util.ServiceUtil;
-import org.thoughtcrime.securesms.util.SignalLocalMetrics;
-import org.thoughtcrime.securesms.util.SmsUtil;
-import org.thoughtcrime.securesms.util.SpanUtil;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.WindowUtil;
-import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
-import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
-import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
-import org.thoughtcrime.securesms.util.views.Stub;
-import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
-import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
-import org.whispersystems.signalservice.api.SignalSessionLock;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
-import io.reactivex.rxjava3.core.Flowable;
-import io.reactivex.rxjava3.core.Single;
-import io.reactivex.rxjava3.core.SingleObserver;
-import io.reactivex.rxjava3.disposables.Disposable;
-import kotlin.Unit;
-
-import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
-
-/**
- * Fragment for displaying a message thread, as well as
- * composing/sending a new message into that thread.
- *
- * @author Moxie Marlinspike
- *
- */
-@SuppressLint("StaticFieldLeak")
-public class ConversationParentFragment extends Fragment
- implements ConversationFragment.ConversationFragmentListener,
- AttachmentManager.AttachmentListener,
- OnKeyboardShownListener,
- InputPanel.Listener,
- InputPanel.MediaListener,
- ComposeText.CursorPositionChangedListener,
- ConversationSearchBottomBar.EventListener,
- StickerEventListener,
- AttachmentKeyboard.Callback,
- ConversationReactionOverlay.OnReactionSelectedListener,
- ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
- SafetyNumberBottomSheet.Callbacks,
- ReactionsBottomSheetDialogFragment.Callback,
- MediaKeyboard.MediaKeyboardListener,
- EmojiEventListener,
- GifKeyboardPageFragment.Host,
- EmojiKeyboardPageFragment.Callback,
- EmojiSearchFragment.Callback,
- StickerKeyboardPageFragment.Callback,
- Material3OnScrollHelperBinder,
- MessageDetailsFragment.Callback,
- ScheduleMessageTimePickerBottomSheet.ScheduleCallback,
- ConversationBottomSheetCallback,
- ScheduleMessageDialogCallback,
- ConversationOptionsMenu.Callback
-{
-
- private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
-
- private static final String TAG = Log.tag(ConversationParentFragment.class);
-
- private static final String STATE_REACT_WITH_ANY_PAGE = "STATE_REACT_WITH_ANY_PAGE";
- private static final String STATE_IS_SEARCH_REQUESTED = "STATE_IS_SEARCH_REQUESTED";
-
- private static final String ARG_INTENT_DATA = "arg.intent.data";
-
- private static final int REQUEST_CODE_SETTINGS = 1000;
-
- private static final int PICK_GALLERY = 1;
- private static final int PICK_DOCUMENT = 2;
- private static final int PICK_AUDIO = 3;
- private static final int PICK_CONTACT = 4;
- private static final int GET_CONTACT_DETAILS = 5;
- private static final int GROUP_EDIT = 6;
- private static final int TAKE_PHOTO = 7;
- private static final int ADD_CONTACT = 8;
- private static final int PICK_LOCATION = 9;
- public static final int PICK_GIF = 10;
- private static final int SMS_DEFAULT = 11;
- private static final int MEDIA_SENDER = 12;
-
- private static final int REQUEST_CODE_PIN_SHORTCUT = 902;
- private static final String ACTION_PINNED_SHORTCUT = "action_pinned_shortcut";
-
- private GlideRequests glideRequests;
- protected ComposeText composeText;
- private AnimatingToggle buttonToggle;
- private SendButton sendButton;
- private ImageButton attachButton;
- private ImageButton sendEditButton;
- protected ConversationTitleView titleView;
- private TextView charactersLeft;
- private ConversationFragment fragment;
- private Button unblockButton;
- private Stub smsExportStub;
- private Stub loggedOutStub;
- private Button registerButton;
- private InputAwareLayout container;
- protected Stub reminderView;
- private Stub unverifiedBannerView;
- private Stub reviewBanner;
- private ComposeTextWatcher typingTextWatcher;
- private ConversationSearchBottomBar searchNav;
- private MenuItem searchViewItem;
- private MessageRequestsBottomView messageRequestBottomView;
- private ConversationReactionDelegate reactionDelegate;
- private Stub voiceNotePlayerViewStub;
- private View navigationBarBackground;
-
- private AttachmentManager attachmentManager;
- private AudioRecorder audioRecorder;
-
- private RecordingSession recordingSession;
- private BroadcastReceiver securityUpdateReceiver;
- private Stub emojiDrawerStub;
- private Stub attachmentKeyboardStub;
- protected HidingLinearLayout quickAttachmentToggle;
- protected HidingLinearLayout inlineAttachmentToggle;
- private InputPanel inputPanel;
- private View noLongerMemberBanner;
- private Stub cannotSendInAnnouncementGroupBanner;
- private View requestingMemberBanner;
- private View cancelJoinRequest;
- private Stub releaseChannelUnmute;
- private Stub mentionsSuggestions;
- private Stub scheduledMessagesBarStub;
- private MaterialButton joinGroupCallButton;
- private boolean callingTooltipShown;
- private ImageView wallpaper;
- private View wallpaperDim;
- private Toolbar toolbar;
- private View toolbarBackground;
- private BroadcastReceiver pinnedShortcutReceiver;
-
- private LinkPreviewViewModel linkPreviewViewModel;
- private ConversationSearchViewModel searchViewModel;
- private ConversationStickerViewModel stickerViewModel;
- private ConversationViewModel viewModel;
- private ConversationGroupViewModel groupViewModel;
- private MentionsPickerViewModel mentionsViewModel;
- private InlineQueryViewModel inlineQueryViewModel;
- private GroupCallViewModel groupCallViewModel;
- private VoiceRecorderWakeLock voiceRecorderWakeLock;
- private DraftViewModel draftViewModel;
- private KeyboardPagerViewModel keyboardPagerViewModel;
- private VoiceNoteMediaController voiceNoteMediaController;
- private VoiceNotePlayerView voiceNotePlayerView;
- private Material3OnScrollHelper material3OnScrollHelper;
- private InlineQueryResultsController inlineQueryResultsController;
- private OnBackPressedCallback backPressedCallback;
-
- private LiveRecipient recipient;
- private long threadId;
- private int distributionType;
- private int reactWithAnyEmojiStartPage = -1;
- private boolean isSearchRequested = false;
- private boolean reshowScheduleMessagesBar = false;
-
- private final LifecycleDisposable disposables = new LifecycleDisposable();
- private final Debouncer optionsMenuDebouncer = new Debouncer(50);
- private final Debouncer textDraftSaveDebouncer = new Debouncer(500);
-
- private IdentityRecordList identityRecords = new IdentityRecordList(Collections.emptyList());
- private Callback callback;
- private RecentEmojiPageModel recentEmojis;
-
- private Set previousPages;
-
- public static ConversationParentFragment create(Intent intent) {
- ConversationParentFragment fragment = new ConversationParentFragment();
- Bundle bundle = new Bundle();
-
- bundle.putAll(ConversationIntents.createParentFragmentArguments(intent));
- fragment.setArguments(bundle);
-
- return fragment;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- SignalLocalMetrics.ConversationOpen.start();
- }
-
- @Override
- public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.conversation_activity, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- disposables.bindTo(getViewLifecycleOwner());
- SpoilerAnnotation.resetRevealedSpoilers();
-
- if (requireActivity() instanceof Callback) {
- callback = (Callback) requireActivity();
- } else if (getParentFragment() instanceof Callback) {
- callback = (Callback) getParentFragment();
- } else {
- throw new ClassCastException("Cannot cast activity or parent fragment into a Callback object");
- }
-
- // TODO [alex] LargeScreenSupport -- This check will no longer be valid / necessary
- if (ConversationIntents.isInvalid(requireArguments())) {
- Log.w(TAG, "[onCreate] Missing recipientId!");
- // TODO [greyson] Navigation
- startActivity(MainActivity.clearTop(requireContext()));
- requireActivity().finish();
- return;
- }
-
- voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), true);
- voiceRecorderWakeLock = new VoiceRecorderWakeLock(requireActivity());
-
- // TODO [alex] LargeScreenSupport -- Should be removed once we move to multi-pane layout.
- new FullscreenHelper(requireActivity()).showSystemUI();
-
- ConversationIntents.Args args = ConversationIntents.Args.from(requireArguments());
- if (savedInstanceState == null && args.getGiftBadge() != null) {
- GiftThanksSheet.show(getChildFragmentManager(), args.getRecipientId(), args.getGiftBadge());
- }
-
- isSearchRequested = args.isWithSearchOpen();
-
- reportShortcutLaunch(args.getRecipientId());
-
- requireActivity().getWindow().getDecorView().setBackgroundResource(R.color.signal_background_primary);
-
- fragment = (ConversationFragment) getChildFragmentManager().findFragmentById(R.id.fragment_content);
- if (fragment == null) {
- fragment = new ConversationFragment();
- getChildFragmentManager().beginTransaction()
- .replace(R.id.fragment_content, fragment)
- .commitNow();
- }
-
- initializeReceivers();
- initializeViews(view);
- updateWallpaper(args.getWallpaper());
- initializeResources(args);
- initializeLinkPreviewObserver();
- initializeSearchObserver();
- initializeStickerObserver();
- initializeViewModel(args);
- initializeGroupViewModel();
- initializeMentionsViewModel();
- initializeGroupCallViewModel();
- initializeDraftViewModel();
- initializeEnabledCheck();
- initializePendingRequestsBanner();
- initializeGroupV1MigrationsBanners();
-
- Flowable observableSecurityInfo = viewModel.getConversationSecurityInfo(args.getRecipientId());
-
- disposables.add(observableSecurityInfo.subscribe(this::handleSecurityChange));
- disposables.add(observableSecurityInfo.firstOrError().subscribe(unused -> onInitialSecurityConfigurationLoaded()));
-
- initializeInsightObserver();
- initializeActionBar();
-
- disposables.add(viewModel.getStoryViewState().subscribe(titleView::setStoryRingFromState));
- disposables.add(viewModel.getScheduledMessageCount().subscribe(this::updateScheduledMessagesBar));
-
- backPressedCallback = new OnBackPressedCallback(true) {
- @Override
- public void handleOnBackPressed() {
- onBackPressed();
- }
- };
- requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
-
- sendButton.post(() -> sendButton.triggerSelectedChangedEvent());
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- // TODO [alex] LargeScreenSupport -- Remove these lines.
- WindowUtil.setLightNavigationBarFromTheme(requireActivity());
- WindowUtil.setLightStatusBarFromTheme(requireActivity());
-
- EventBus.getDefault().register(this);
- backPressedCallback.setEnabled(true);
- viewModel.checkIfMmsIsEnabled();
- initializeIdentityRecords();
- composeText.setMessageSendType(sendButton.getSelectedSendType());
-
- Recipient recipientSnapshot = recipient.get();
-
- titleView.setTitle(glideRequests, recipientSnapshot);
- setBlockedUserState(recipientSnapshot, viewModel.getConversationStateSnapshot().getSecurityInfo());
- calculateCharactersRemaining();
-
- if (recipientSnapshot.getGroupId().isPresent() && recipientSnapshot.getGroupId().get().isV2() && !recipientSnapshot.isBlocked()) {
- GroupId.V2 groupId = recipientSnapshot.getGroupId().get().requireV2();
-
- ApplicationDependencies.getJobManager()
- .startChain(new RequestGroupV2InfoJob(groupId))
- .then(GroupV2UpdateSelfProfileKeyJob.withoutLimits(groupId))
- .enqueue();
-
- ForceUpdateGroupV2Job.enqueueIfNecessary(groupId);
-
- if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
- groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getChildFragmentManager(), groupId);
- }
- }
-
- if (groupCallViewModel != null) {
- groupCallViewModel.peekGroupCall();
- }
-
- setVisibleThread(threadId);
- ConversationUtil.refreshRecipientShortcuts();
-
- if (SignalStore.rateLimit().needsRecaptcha()) {
- RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
- }
-
- updateToggleButtonState();
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (!isInBubble()) {
- ApplicationDependencies.getMessageNotifier().clearVisibleThread();
- }
-
- if (requireActivity().isFinishing()) requireActivity().overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_end);
- inputPanel.onPause();
-
- fragment.setLastSeen(System.currentTimeMillis());
- markLastSeen();
- EventBus.getDefault().unregister(this);
- material3OnScrollHelper.setColorImmediate();
- }
-
- @Override
- public void onStop() {
- super.onStop();
- EventBus.getDefault().unregister(this);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- Log.i(TAG, "onConfigurationChanged(" + newConfig.orientation + ")");
- super.onConfigurationChanged(newConfig);
- composeText.setMessageSendType(sendButton.getSelectedSendType());
-
- if (emojiDrawerStub.resolved() && container.getCurrentInput() == emojiDrawerStub.get()) {
- container.hideAttachedInput(true);
- }
-
- if (reactionDelegate.isShowing()) {
- reactionDelegate.hide();
- }
-
- if (inlineQueryResultsController != null) {
- inlineQueryResultsController.onOrientationChange(newConfig.orientation == ORIENTATION_LANDSCAPE);
- }
- }
-
- @Override
- public void onDestroy() {
- if (securityUpdateReceiver != null) requireActivity().unregisterReceiver(securityUpdateReceiver);
- if (pinnedShortcutReceiver != null) requireActivity().unregisterReceiver(pinnedShortcutReceiver);
- super.onDestroy();
- }
-
- // TODO [alex] LargeScreenSupport -- Pipe in events from activity
- public boolean dispatchTouchEvent(MotionEvent ev) {
- return reactionDelegate.applyTouchEvent(ev);
- }
-
- @Override
- public void onActivityResult(final int reqCode, int resultCode, Intent data) {
- Log.i(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
- super.onActivityResult(reqCode, resultCode, data);
-
- if ((data == null && reqCode != TAKE_PHOTO && reqCode != SMS_DEFAULT) ||
- (resultCode != Activity.RESULT_OK && reqCode != SMS_DEFAULT))
- {
- updateLinkPreviewState();
- SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(requireContext()));
- return;
- }
-
- switch (reqCode) {
- case PICK_DOCUMENT:
- setMedia(data.getData(), MediaType.DOCUMENT);
- break;
- case PICK_AUDIO:
- setMedia(data.getData(), MediaType.AUDIO);
- break;
- case PICK_CONTACT:
- if (viewModel.isPushAvailable() && !isSmsForced()) {
- openContactShareEditor(data.getData());
- } else {
- addAttachmentContactInfo(data.getData());
- }
- break;
- case GET_CONTACT_DETAILS:
- sendSharedContact(data.getParcelableArrayListExtra(ContactShareEditActivity.KEY_CONTACTS));
- break;
- case GROUP_EDIT:
- Recipient recipientSnapshot = recipient.get();
-
- onRecipientChanged(recipientSnapshot);
- titleView.setTitle(glideRequests, recipientSnapshot);
- NotificationChannels.getInstance().updateContactChannelName(recipientSnapshot);
- setBlockedUserState(recipientSnapshot, viewModel.getConversationStateSnapshot().getSecurityInfo());
- invalidateOptionsMenu();
- break;
- case TAKE_PHOTO:
- handleImageFromDeviceCameraApp();
- break;
- case ADD_CONTACT:
- SimpleTask.run(() -> {
- try {
- ContactDiscovery.refresh(requireContext(), recipient.get(), false, TimeUnit.SECONDS.toMillis(10));
- } catch (IOException e) {
- Log.w(TAG, "Failed to refresh user after adding to contacts.");
- }
- return null;
- }, nothing -> onRecipientChanged(recipient.get()));
- break;
- case PICK_LOCATION:
- if (data.getData() != null) {
- SignalPlace place = new SignalPlace(PlacePickerActivity.addressFromData(data));
- attachmentManager.setLocation(place, data.getData());
- draftViewModel.setLocationDraft(place);
- } else {
- Log.w(TAG, "Location missing thumbnail");
- }
- break;
- case SMS_DEFAULT:
- viewModel.updateSecurityInfo();
- break;
- case PICK_GIF:
- case MEDIA_SENDER:
- MediaSendActivityResult result = MediaSendActivityResult.fromData(data);
-
- if (!Objects.equals(result.getRecipientId(), recipient.getId())) {
- Log.w(TAG, "Result's recipientId did not match ours! Result: " + result.getRecipientId() + ", Activity: " + recipient.getId());
- Toast.makeText(requireContext(), R.string.ConversationActivity_error_sending_media, Toast.LENGTH_SHORT).show();
- return;
- }
-
- sendButton.setSendType(result.getMessageSendType());
-
- if (result.isPushPreUpload()) {
- sendMediaMessage(result);
- return;
- }
-
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- boolean initiating = threadId == -1;
- QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
- SlideDeck slideDeck = new SlideDeck();
- List mentions = new ArrayList<>(result.getMentions());
- BodyRangeList bodyRanges = result.getBodyRanges();
-
- for (Media mediaItem : result.getNonUploadedMedia()) {
- if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
- slideDeck.addSlide(new VideoSlide(requireContext(), mediaItem.getUri(), mediaItem.getSize(), mediaItem.isVideoGif(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orElse(null), mediaItem.getTransformProperties().orElse(null)));
- } else if (MediaUtil.isGif(mediaItem.getMimeType())) {
- slideDeck.addSlide(new GifSlide(requireContext(), mediaItem.getUri(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orElse(null)));
- } else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
- slideDeck.addSlide(new ImageSlide(requireContext(), mediaItem.getUri(), mediaItem.getMimeType(), mediaItem.getSize(), mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.isBorderless(), mediaItem.getCaption().orElse(null), null, mediaItem.getTransformProperties().orElse(null)));
- } else {
- Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
- }
- }
-
- final Context context = requireContext().getApplicationContext();
-
- sendMediaMessage(result.getRecipientId(),
- result.getMessageSendType(),
- result.getBody(),
- slideDeck,
- quote,
- Collections.emptyList(),
- Collections.emptyList(),
- mentions,
- bodyRanges,
- expiresIn,
- result.isViewOnce(),
- initiating,
- true,
- null,
- result.getScheduledTime(),
- null).addListener(new AssertedSuccessListener() {
- @Override
- public void onSuccess(Void result) {
- AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
- Stream.of(slideDeck.getSlides())
- .map(Slide::getUri)
- .withoutNulls()
- .filter(BlobProvider::isAuthority)
- .forEach(uri -> BlobProvider.getInstance().delete(context, uri));
- });
- }
- });
-
- break;
- }
- }
-
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
-
- outState.putInt(STATE_REACT_WITH_ANY_PAGE, reactWithAnyEmojiStartPage);
- outState.putBoolean(STATE_IS_SEARCH_REQUESTED, isSearchRequested);
- }
-
- @Override
- public void onViewStateRestored(Bundle savedInstanceState) {
- super.onViewStateRestored(savedInstanceState);
-
- if (savedInstanceState != null) {
- reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, -1);
- isSearchRequested = savedInstanceState.getBoolean(STATE_IS_SEARCH_REQUESTED, false);
- }
- }
-
- private void onInitialSecurityConfigurationLoaded() {
- Log.d(TAG, "Initial security configuration loaded.");
- if (getContext() == null) {
- Log.w(TAG, "Fragment has become detached from context. Ignoring configuration call.");
- return;
- }
-
- initializeProfiles();
- initializeGv1Migration();
-
- Log.d(TAG, "Initializing draft from initial security configuration load...");
- initializeDraft(viewModel.getArgs()).addListener(new AssertedSuccessListener() {
- @Override
- public void onSuccess(Boolean loadedDraft) {
- Log.d(TAG, "Initial security configuration loaded.");
- if (getContext() == null) {
- Log.w(TAG, "Fragment has become detached from context. Ignoring draft load.");
- return;
- }
-
- if (loadedDraft != null && loadedDraft) {
- Log.i(TAG, "Finished loading draft");
- ThreadUtil.runOnMain(() -> {
- if (fragment != null && fragment.isResumed()) {
- fragment.moveToLastSeen();
- } else {
- Log.w(TAG, "Wanted to move to the last seen position, but the fragment was in an invalid state");
- }
- });
- }
-
- composeText.addTextChangedListener(typingTextWatcher);
- composeText.setStylingChangedListener(typingTextWatcher);
- composeText.setSelection(composeText.length(), composeText.length());
- }
- });
- }
-
- private void setVisibleThread(long threadId) {
- if (!isInBubble()) {
- // TODO [alex] LargeScreenSupport -- Inform MainActivityViewModel that the conversation was opened.
- ApplicationDependencies.getMessageNotifier().setVisibleThread(ConversationId.forConversation(threadId));
- }
- }
-
- private void reportShortcutLaunch(@NonNull RecipientId recipientId) {
- ShortcutManagerCompat.reportShortcutUsed(requireContext(), ConversationUtil.getShortcutId(recipientId));
- }
-
- private void handleImageFromDeviceCameraApp() {
- if (attachmentManager.getCaptureUri() == null) {
- Log.w(TAG, "No image available.");
- return;
- }
-
- try {
- Uri mediaUri = BlobProvider.getInstance()
- .forData(requireContext().getContentResolver().openInputStream(attachmentManager.getCaptureUri()), 0L)
- .withMimeType(MediaUtil.IMAGE_JPEG)
- .createForSingleSessionOnDisk(requireContext());
-
- requireContext().getContentResolver().delete(attachmentManager.getCaptureUri(), null, null);
-
- setMedia(mediaUri, MediaType.IMAGE);
- } catch (IOException ioe) {
- Log.w(TAG, "Could not handle public image", ioe);
- }
- }
-
- @Override
- public void startActivity(Intent intent) {
- if (intent.getStringExtra(Browser.EXTRA_APPLICATION_ID) != null) {
- intent.removeExtra(Browser.EXTRA_APPLICATION_ID);
- }
-
- try {
- super.startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, e);
- Toast.makeText(requireContext(), R.string.ConversationActivity_there_is_no_app_available_to_handle_this_link_on_your_device, Toast.LENGTH_LONG).show();
- }
- }
-
- @Override
- public void onOptionsMenuCreated(@NonNull Menu menu) {
- searchViewItem = menu.findItem(R.id.menu_search);
-
- SearchView searchView = (SearchView) searchViewItem.getActionView();
- SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
- @Override
- public boolean onQueryTextSubmit(String query) {
- searchViewModel.onQueryUpdated(query, threadId, true);
- searchNav.showLoading();
- viewModel.setSearchQuery(query);
- return true;
- }
-
- @Override
- public boolean onQueryTextChange(String query) {
- searchViewModel.onQueryUpdated(query, threadId, false);
- searchNav.showLoading();
- viewModel.setSearchQuery(query);
- return true;
- }
- };
-
- searchViewItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
- @Override
- public boolean onMenuItemActionExpand(MenuItem item) {
- searchView.setOnQueryTextListener(queryListener);
- isSearchRequested = true;
- searchViewModel.onSearchOpened();
- searchNav.setVisibility(View.VISIBLE);
- searchNav.setData(0, 0);
- inputPanel.setHideForSearch(true);
-
- for (int i = 0; i < menu.size(); i++) {
- if (!menu.getItem(i).equals(searchViewItem)) {
- menu.getItem(i).setVisible(false);
- }
- }
- return true;
- }
-
- @Override
- public boolean onMenuItemActionCollapse(MenuItem item) {
- searchView.setOnQueryTextListener(null);
- isSearchRequested = false;
- searchViewModel.onSearchClosed();
- searchNav.setVisibility(View.GONE);
- inputPanel.setHideForSearch(false);
- viewModel.setSearchQuery(null);
- setBlockedUserState(recipient.get(), viewModel.getConversationStateSnapshot().getSecurityInfo());
- invalidateOptionsMenu();
- return true;
- }
- });
-
- searchView.setMaxWidth(Integer.MAX_VALUE);
-
- if (isSearchRequested) {
- if (searchViewItem.expandActionView()) {
- searchViewModel.onSearchOpened();
- }
- }
-
- int toolbarTextAndIconColor = getResources().getColor(wallpaper.getDrawable() != null ? R.color.signal_colorNeutralInverse : R.color.signal_colorOnSurface);
- setToolbarActionItemTint(toolbar, toolbarTextAndIconColor);
- }
-
- public void invalidateOptionsMenu() {
- if (!isSearchRequested && getActivity() != null) {
- optionsMenuDebouncer.publish(() -> {
- if (getActivity() != null) {
- toolbar.invalidateMenu();
- }
- });
- }
- }
-
- public void onBackPressed() {
- Log.d(TAG, "onBackPressed()");
- if (reactionDelegate.isShowing()) {
- reactionDelegate.hide();
- } else if (container.isInputOpen()) {
- container.hideCurrentInput(composeText);
- navigationBarBackground.setVisibility(View.GONE);
- } else if (isSearchRequested) {
- if (searchViewItem != null) {
- searchViewItem.collapseActionView();
- }
- } else if (isInBubble()) {
- backPressedCallback.setEnabled(false);
- requireActivity().onBackPressed();
- } else {
- requireActivity().finish();
- }
- }
-
- @Override
- public void onKeyboardShown() {
- inputPanel.onKeyboardShown();
- if (emojiDrawerStub.resolved() && emojiDrawerStub.get().isShowing()) {
- if (emojiDrawerStub.get().isEmojiSearchMode()) {
- inputPanel.setToIme();
- } else {
- emojiDrawerStub.get().hide(true);
- }
- }
- if (attachmentKeyboardStub.resolved() && attachmentKeyboardStub.get().isShowing()) {
- navigationBarBackground.setVisibility(View.GONE);
- attachmentKeyboardStub.get().hide(true);
- }
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onEvent(ReminderUpdateEvent event) {
- updateReminders();
- }
-
- @SuppressLint("MissingSuperCall")
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
- }
-
- @Override
- public void onAttachmentMediaClicked(@NonNull Media media) {
- linkPreviewViewModel.onUserCancel();
- startActivityForResult(MediaSelectionActivity.editor(requireActivity(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
- container.hideCurrentInput(composeText);
- }
-
- @Override
- public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) {
- switch (button) {
- case GALLERY:
- AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedSendType(), inputPanel.getQuote().isPresent());
- break;
- case FILE:
- AttachmentManager.selectDocument(this, PICK_DOCUMENT);
- break;
- case CONTACT:
- AttachmentManager.selectContactInfo(this, PICK_CONTACT);
- break;
- case LOCATION:
- AttachmentManager.selectLocation(this, PICK_LOCATION, getSendButtonColor(sendButton.getSelectedSendType()));
- break;
- case PAYMENT:
- AttachmentManager.selectPayment(this, recipient.get());
- break;
-
- }
-
- container.hideCurrentInput(composeText);
- }
-
- @Override
- public void onAttachmentPermissionsRequested() {
- Permissions.with(this)
- .request(Manifest.permission.READ_EXTERNAL_STORAGE)
- .onAllGranted(() -> viewModel.onAttachmentKeyboardOpen())
- .withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
- .execute();
- }
-
-//////// Event Handlers
-
- @Override
- public void handleSelectMessageExpiration() {
- if (isPushGroupConversation() && !isActiveGroup()) {
- return;
- }
-
- startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipient.getId()));
- }
-
- @Override
- public void handleMuteNotifications() {
- MuteDialog.show(requireActivity(), viewModel::muteConversation);
- }
-
- private void handleStoryRingClick() {
- startActivity(StoryViewerActivity.createIntent(
- requireContext(),
- new StoryViewerArgs.Builder(recipient.getId(), recipient.get().shouldHideStory())
- .isFromQuote(true)
- .build()));
- }
-
- @Override
- public void handleConversationSettings() {
- if (isGroupConversation()) {
- handleManageGroup();
- return;
- }
-
- if (isInMessageRequest() && !recipient.get().isBlocked()) return;
-
- Intent intent = ConversationSettingsActivity.forRecipient(requireContext(), recipient.getId());
- Bundle bundle = ConversationSettingsActivity.createTransitionBundle(requireActivity(), titleView.findViewById(R.id.contact_photo_image), toolbar);
-
- ActivityCompat.startActivity(requireActivity(), intent, bundle);
- }
-
- @Override
- public void handleUnmuteNotifications() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- SignalDatabase.recipients().setMuted(recipient.getId(), 0);
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private void handleUnblock() {
- final Context context = requireContext().getApplicationContext();
- BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient.get(), () -> {
- SignalExecutors.BOUNDED.execute(() -> {
- RecipientUtil.unblock(recipient.get());
- });
- });
- }
-
- private void handleMakeDefaultSms() {
- startActivityForResult(SmsUtil.getSmsRoleIntent(requireContext()), SMS_DEFAULT);
- }
-
- private void handleRegisterForSignal() {
- startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
- }
-
- @Override
- public void handleInviteLink() {
- InviteActions.INSTANCE.inviteUserToSignal(
- requireContext(),
- recipient.get(),
- text -> {
- composeText.appendInvite(text);
- return Unit.INSTANCE;
- },
- intent -> {
- startActivity(intent);
- return Unit.INSTANCE;
- }
- );
- }
-
- @Override
- public void handleViewMedia() {
- startActivity(MediaOverviewActivity.forThread(requireContext(), threadId));
- }
-
- @Override
- public void handleAddShortcut() {
- Log.i(TAG, "Creating home screen shortcut for recipient " + recipient.get().getId());
-
- final Context context = requireContext().getApplicationContext();
- final Recipient recipient = this.recipient.get();
-
- if (pinnedShortcutReceiver == null) {
- pinnedShortcutReceiver = new BroadcastReceiver() {
- @Override public void onReceive(Context context, Intent intent) {
- Toast.makeText(context, context.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show();
- }
- };
- requireActivity().registerReceiver(pinnedShortcutReceiver, new IntentFilter(ACTION_PINNED_SHORTCUT));
- }
-
- GlideApp.with(this)
- .asBitmap()
- .load(recipient.getContactPhoto())
- .error(recipient.getFallbackContactPhoto().asDrawable(context, recipient.getAvatarColor(), false))
- .into(new CustomTarget() {
- @Override
- public void onLoadFailed(@Nullable Drawable errorDrawable) {
- if (errorDrawable == null) {
- throw new AssertionError();
- }
-
- Log.w(TAG, "Utilizing fallback photo for shortcut for recipient " + recipient.getId());
-
- SimpleTask.run(() -> DrawableUtil.toBitmap(errorDrawable, SHORTCUT_ICON_SIZE, SHORTCUT_ICON_SIZE),
- bitmap -> addIconToHomeScreen(context, bitmap, recipient));
- }
-
- @Override
- public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition super Bitmap> transition) {
- SimpleTask.run(() -> BitmapUtil.createScaledBitmap(resource, SHORTCUT_ICON_SIZE, SHORTCUT_ICON_SIZE),
- bitmap -> addIconToHomeScreen(context, bitmap, recipient));
- }
-
- @Override
- public void onLoadCleared(@Nullable Drawable placeholder) {
- }
- });
-
- }
-
- @Override
- public void handleCreateBubble() {
- ConversationIntents.Args args = viewModel.getArgs();
-
- BubbleUtil.displayAsBubble(requireContext(), args.getRecipientId(), args.getThreadId());
- requireActivity().finish();
- }
-
- private static void addIconToHomeScreen(@NonNull Context context,
- @NonNull Bitmap bitmap,
- @NonNull Recipient recipient)
- {
- IconCompat icon = IconCompat.createWithAdaptiveBitmap(bitmap);
- String name = recipient.isSelf() ? context.getString(R.string.note_to_self)
- : recipient.getDisplayName(context);
-
- ShortcutInfoCompat shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, recipient.getId().serialize() + '-' + System.currentTimeMillis())
- .setShortLabel(name)
- .setIcon(icon)
- .setIntent(ShortcutLauncherActivity.createIntent(context, recipient.getId()))
- .build();
-
- Intent callbackIntent = new Intent(ACTION_PINNED_SHORTCUT);
- PendingIntent shortcutPinnedCallback = PendingIntent.getBroadcast(context, REQUEST_CODE_PIN_SHORTCUT, callbackIntent, PendingIntentFlags.mutable());
-
- ShortcutManagerCompat.requestPinShortcut(context, shortcutInfoCompat, shortcutPinnedCallback.getIntentSender());
-
- bitmap.recycle();
- }
-
- @Override
- public void handleSearch() {
- searchViewModel.onSearchOpened();
- }
-
- @Override
- public void handleLeavePushGroup() {
- if (getRecipient() == null) {
- Toast.makeText(requireContext(), getString(R.string.ConversationActivity_invalid_recipient),
- Toast.LENGTH_LONG).show();
- return;
- }
-
- LeaveGroupDialog.handleLeavePushGroup(requireActivity(), getRecipient().requireGroupId().requirePush(), () -> requireActivity().finish());
- }
-
- @Override
- public void handleManageGroup() {
- Intent intent = ConversationSettingsActivity.forGroup(requireContext(), recipient.get().requireGroupId());
- Bundle bundle = ConversationSettingsActivity.createTransitionBundle(requireContext(), titleView.findViewById(R.id.contact_photo_image), toolbar);
-
- ActivityCompat.startActivity(requireContext(), intent, bundle);
- }
-
- @Override
- public void handleDistributionBroadcastEnabled(MenuItem item) {
- distributionType = ThreadTable.DistributionTypes.BROADCAST;
- draftViewModel.setDistributionType(distributionType);
- viewModel.setDistributionType(distributionType);
- item.setChecked(true);
- }
-
- @Override
- public void handleDistributionConversationEnabled(MenuItem item) {
- distributionType = ThreadTable.DistributionTypes.CONVERSATION;
- draftViewModel.setDistributionType(distributionType);
- viewModel.setDistributionType(distributionType);
- item.setChecked(true);
- }
-
- @Override
- public void handleDial(boolean isSecure) {
- Recipient recipient = getRecipient();
-
- if (isSecure) {
- CommunicationActions.startVoiceCall(this, recipient);
- } else {
- CommunicationActions.startInsecureCall(this, recipient);
- }
- }
-
- @Override
- public void handleVideo() {
- Recipient recipient = getRecipient();
-
- if (recipient.isPushV2Group() && groupCallViewModel.hasActiveGroupCall().getValue() == Boolean.FALSE && groupViewModel.isNonAdminInAnnouncementGroup()) {
- new MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.ConversationActivity_cant_start_group_call)
- .setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
- .setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss())
- .show();
- } else {
- CommunicationActions.startVideoCall(this, recipient);
- }
- }
-
- @Override
- public void handleDisplayGroupRecipients() {
- new GroupMembersDialog(requireActivity(), getRecipient()).display();
- }
-
- @Override
- public void handleAddToContacts() {
- if (recipient.get().isGroup()) return;
-
- try {
- startActivityForResult(RecipientExporter.export(recipient.get()).asAddContactIntent(), ADD_CONTACT);
- } catch (ActivityNotFoundException e) {
- Log.w(TAG, e);
- }
- }
-
- private boolean handleDisplayQuickContact() {
- if (isInMessageRequest() || recipient.get().isGroup()) return false;
-
- if (recipient.get().getContactUri() != null) {
- ContactsContract.QuickContact.showQuickContact(requireContext(), titleView, recipient.get().getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null);
- } else {
- handleAddToContacts();
- }
-
- return true;
- }
-
- private void handleAddAttachment() {
- if (viewModel.getConversationStateSnapshot().isMmsEnabled() || viewModel.isPushAvailable()) {
- viewModel.getRecentMedia().removeObservers(this);
-
- if (attachmentKeyboardStub.resolved() && container.isInputOpen() && container.getCurrentInput() == attachmentKeyboardStub.get()) {
- container.showSoftkey(composeText);
- } else {
- viewModel.getRecentMedia().observe(getViewLifecycleOwner(), media -> attachmentKeyboardStub.get().onMediaChanged(media));
- attachmentKeyboardStub.get().setCallback(this);
- attachmentKeyboardStub.get().setWallpaperEnabled(recipient.get().hasWallpaper());
-
- updatePaymentsAvailable();
-
- container.show(composeText, attachmentKeyboardStub.get());
- navigationBarBackground.setVisibility(View.VISIBLE);
-
- viewModel.onAttachmentKeyboardOpen();
- }
- } else {
- handleManualMmsRequired();
- }
- }
-
- private void updatePaymentsAvailable() {
- if (!attachmentKeyboardStub.resolved()) {
- return;
- }
-
- PaymentsValues paymentsValues = SignalStore.paymentsValues();
-
- if (paymentsValues.getPaymentsAvailability().isSendAllowed() &&
- !recipient.get().isSelf() &&
- !recipient.get().isGroup() &&
- recipient.get().isRegistered())
- {
- attachmentKeyboardStub.get().filterAttachmentKeyboardButtons(null);
- } else {
- attachmentKeyboardStub.get().filterAttachmentKeyboardButtons(btn -> btn != AttachmentKeyboardButton.PAYMENT);
- }
- }
-
- private void handleManualMmsRequired() {
- Toast.makeText(requireContext(), R.string.MmsDownloader_error_reading_mms_settings, Toast.LENGTH_LONG).show();
-
- Bundle extras = requireArguments();
- Intent intent = new Intent(requireContext(), PromptMmsActivity.class);
-
- intent.putExtras(extras);
- startActivity(intent);
- }
-
- private void handleRecentSafetyNumberChange() {
- List records = identityRecords.getUnverifiedRecords();
- records.addAll(identityRecords.getUntrustedRecords());
- SafetyNumberBottomSheet
- .forIdentityRecordsAndDestination(
- records,
- new ContactSearchKey.RecipientSearchKey(recipient.getId(), false)
- )
- .show(getChildFragmentManager());
- }
-
- @Override
- public void onMessageResentAfterSafetyNumberChangeInBottomSheet() {
- Log.d(TAG, "onMessageResentAfterSafetyNumberChange");
- initializeIdentityRecords().addListener(new AssertedSuccessListener() {
- @Override
- public void onSuccess(Boolean result) { }
- });
- }
-
- @Override
- public void onCanceled() { }
-
- private void handleSecurityChange(@NonNull ConversationSecurityInfo conversationSecurityInfo) {
- Log.i(TAG, "handleSecurityChange(" + conversationSecurityInfo + ")");
-
- boolean isPushAvailable = conversationSecurityInfo.isPushAvailable();
- boolean isMediaMessage = recipient.get().isMmsGroup() || attachmentManager.isAttachmentPresent();
-
- sendButton.resetAvailableTransports(isMediaMessage);
-
- boolean smsEnabled = true;
-
- if (recipient.get().isPushGroup() || (!recipient.get().isMmsGroup() && !recipient.get().hasSmsAddress())) {
- sendButton.disableTransportType(MessageSendType.TransportType.SMS);
- smsEnabled = false;
- }
-
- if (!isPushAvailable && !isPushGroupConversation() && !recipient.get().isServiceIdOnly() && !recipient.get().isReleaseNotes() && smsEnabled) {
- sendButton.disableTransportType(MessageSendType.TransportType.SIGNAL);
- }
-
- if (isPushAvailable || isPushGroupConversation() || recipient.get().isServiceIdOnly() || recipient.get().isReleaseNotes() || !smsEnabled) {
- sendButton.setDefaultTransport(MessageSendType.TransportType.SIGNAL);
- } else {
- sendButton.setDefaultTransport(MessageSendType.TransportType.SMS);
- viewModel.insertSmsExportUpdateEvent(recipient.get());
- }
-
- calculateCharactersRemaining();
- invalidateOptionsMenu();
- setBlockedUserState(recipient.get(), conversationSecurityInfo);
- onSecurityUpdated();
- }
-
- ///// Initializers
-
- private ListenableFuture initializeDraft(@NonNull ConversationIntents.Args args) {
- final SettableFuture result = new SettableFuture<>();
-
- long sharedDataTimestamp = args.getShareDataTimestamp();
- long lastTimestamp = callback.getShareDataTimestamp();
- boolean hasProcessedShareData = sharedDataTimestamp > 0 && sharedDataTimestamp <= lastTimestamp;
-
- Log.d(TAG, "Shared this data at " + sharedDataTimestamp + " and last processed share data at " + lastTimestamp);
- if (hasProcessedShareData) {
- Log.d(TAG, "Already processed this share data. Skipping.");
- result.set(false);
- return result;
- } else {
- Log.d(TAG, "Have not processed this share data. Proceeding.");
- callback.setShareDataTimestamp(sharedDataTimestamp);
- }
-
- final CharSequence draftText = args.getDraftText();
- final Uri draftMedia = ConversationIntents.getIntentData(requireArguments());
- final String draftContentType = ConversationIntents.getIntentType(requireArguments());
- final MediaType draftMediaType = MediaType.from(draftContentType);
- final List mediaList = args.getMedia();
- final StickerLocator stickerLocator = args.getStickerLocator();
- final boolean borderless = args.isBorderless();
-
- if (stickerLocator != null && draftMedia != null) {
- Log.d(TAG, "Handling shared sticker.");
- sendSticker(stickerLocator, Objects.requireNonNull(draftContentType), draftMedia, 0, true);
- return new SettableFuture<>(false);
- }
-
- if (draftMedia != null && draftContentType != null && borderless) {
- Log.d(TAG, "Handling borderless draft media with content type " + draftContentType);
- SimpleTask.run(getLifecycle(),
- () -> getKeyboardImageDetails(draftMedia),
- details -> sendKeyboardImage(draftMedia, draftContentType, details));
- return new SettableFuture<>(false);
- }
-
- if (!Util.isEmpty(mediaList)) {
- Log.d(TAG, "Handling shared Media.");
- Intent sendIntent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), mediaList, recipient.getId(), draftText);
- startActivityForResult(sendIntent, MEDIA_SENDER);
- return new SettableFuture<>(false);
- }
-
- if (draftText != null) {
- Log.d(TAG, "Handling shared text");
- composeText.setText("");
- composeText.append(draftText);
- result.set(true);
- }
-
- if (draftMedia != null && draftMediaType != null) {
- Log.d(TAG, "Handling shared Data.");
- return setMedia(draftMedia, draftMediaType);
- }
-
- if (draftText == null && (draftMedia == null || ConversationIntents.isBubbleIntentUri(draftMedia) || ConversationIntents.isNotificationIntentUri(draftMedia)) && draftMediaType == null) {
- Log.d(TAG, "Initializing draft from database");
- return initializeDraftFromDatabase();
- } else {
- updateToggleButtonState();
- result.set(false);
- }
-
- return result;
- }
-
- private void initializeEnabledCheck() {
- groupViewModel.getSelfMemberLevel().observe(getViewLifecycleOwner(), selfMembership -> {
- boolean canSendMessages;
- boolean leftGroup;
- boolean canCancelRequest;
-
- if (selfMembership == null) {
- leftGroup = false;
- canSendMessages = true;
- canCancelRequest = false;
- if (cannotSendInAnnouncementGroupBanner.resolved()) {
- cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
- }
- } else {
- switch (selfMembership.getMemberLevel()) {
- case NOT_A_MEMBER:
- leftGroup = true;
- canSendMessages = false;
- canCancelRequest = false;
- break;
- case PENDING_MEMBER:
- leftGroup = false;
- canSendMessages = false;
- canCancelRequest = false;
- break;
- case REQUESTING_MEMBER:
- leftGroup = false;
- canSendMessages = false;
- canCancelRequest = true;
- break;
- case FULL_MEMBER:
- case ADMINISTRATOR:
- leftGroup = false;
- canSendMessages = true;
- canCancelRequest = false;
- break;
- default:
- throw new AssertionError();
- }
-
- if (!leftGroup && !canCancelRequest && selfMembership.isAnnouncementGroup() && selfMembership.getMemberLevel() != GroupTable.MemberLevel.ADMINISTRATOR) {
- canSendMessages = false;
- cannotSendInAnnouncementGroupBanner.get().setVisibility(View.VISIBLE);
- cannotSendInAnnouncementGroupBanner.get().setMovementMethod(LinkMovementMethod.getInstance());
- cannotSendInAnnouncementGroupBanner.get().setText(SpanUtil.clickSubstring(requireContext(), R.string.ConversationActivity_only_s_can_send_messages, R.string.ConversationActivity_admins, v -> {
- ShowAdminsBottomSheetDialog.show(getChildFragmentManager(), getRecipient().requireGroupId().requireV2());
- }));
- } else if (cannotSendInAnnouncementGroupBanner.resolved()) {
- cannotSendInAnnouncementGroupBanner.get().setVisibility(View.GONE);
- }
- }
-
- if (messageRequestBottomView.getVisibility() == View.GONE) {
- noLongerMemberBanner.setVisibility(leftGroup ? View.VISIBLE : View.GONE);
- }
-
- requestingMemberBanner.setVisibility(canCancelRequest ? View.VISIBLE : View.GONE);
-
- if (canCancelRequest) {
- cancelJoinRequest.setOnClickListener(v -> ConversationGroupViewModel.onCancelJoinRequest(getRecipient(), new AsynchronousCallback.MainThread() {
- @Override
- public void onComplete(@Nullable Void result) {
- Log.d(TAG, "Cancel request complete");
- }
-
- @Override
- public void onError(@Nullable GroupChangeFailureReason error) {
- Log.d(TAG, "Cancel join request failed " + error);
- Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(error), Toast.LENGTH_SHORT).show();
- }
- }.toWorkerCallback()));
- }
-
- inputPanel.setHideForGroupState(!canSendMessages);
- inputPanel.setEnabled(canSendMessages);
- sendButton.setEnabled(canSendMessages);
- attachButton.setEnabled(canSendMessages);
- sendEditButton.setEnabled(canSendMessages);
- });
- }
-
- private void initializePendingRequestsBanner() {
- groupViewModel.getActionableRequestingMembers()
- .observe(getViewLifecycleOwner(), actionablePendingGroupRequests -> updateReminders());
- }
-
- private void initializeGroupV1MigrationsBanners() {
- groupViewModel.getGroupV1MigrationSuggestions()
- .observe(getViewLifecycleOwner(), s -> updateReminders());
- }
-
- private ListenableFuture initializeDraftFromDatabase() {
- SettableFuture future = new SettableFuture<>();
-
- Disposable disposable = draftViewModel
- .loadDrafts(threadId)
- .subscribe(databaseDrafts -> {
- Drafts drafts = databaseDrafts.getDrafts();
- CharSequence updatedText = databaseDrafts.getUpdatedText();
-
- if (drafts.isEmpty()) {
- future.set(false);
- updateToggleButtonState();
- return;
- }
-
- AtomicInteger draftsRemaining = new AtomicInteger(drafts.size());
- AtomicBoolean success = new AtomicBoolean(false);
- ListenableFuture.Listener listener = new AssertedSuccessListener() {
- @Override
- public void onSuccess(Boolean result) {
- success.compareAndSet(false, result);
-
- if (draftsRemaining.decrementAndGet() <= 0) {
- future.set(success.get());
- }
- }
- };
-
- for (Draft draft : drafts) {
- try {
- switch (draft.getType()) {
- case Draft.TEXT:
- composeText.setText(updatedText == null ? draft.getValue() : updatedText);
- listener.onSuccess(true);
- break;
- case Draft.LOCATION:
- attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
- break;
- case Draft.IMAGE:
- setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
- break;
- case Draft.AUDIO:
- setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
- break;
- case Draft.VIDEO:
- setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
- break;
- case Draft.QUOTE:
- SettableFuture quoteResult = new SettableFuture<>();
- disposables.add(draftViewModel.loadDraftQuote(draft.getValue()).subscribe(
- conversationMessage -> {
- handleReplyMessage(conversationMessage);
- quoteResult.set(true);
- },
- err -> {
- Log.e(TAG, "Failed to restore a quote from a draft.", err);
- quoteResult.set(false);
- },
- () -> {
- Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");
- quoteResult.set(false);
- }
- ));
-
- quoteResult.addListener(listener);
- break;
- case Draft.MESSAGE_EDIT:
- SettableFuture messageEditResult = new SettableFuture<>();
- disposables.add(draftViewModel.loadDraftEditMessage(draft.getValue()).subscribe(
- conversationMessage -> {
- inputPanel.enterEditMessageMode(glideRequests, conversationMessage, true);
- messageEditResult.set(true);
- },
- err -> {
- Log.e(TAG, "Failed to restore message edit from a draft.", err);
- messageEditResult.set(false);
- },
- () -> {
- Log.e(TAG, "Failed to load message edit. No matching message record.");
- messageEditResult.set(false);
- }
- ));
- messageEditResult.addListener(listener);
- break;
- case Draft.VOICE_NOTE:
- case Draft.BODY_RANGES:
- listener.onSuccess(true);
- break;
- }
- } catch (IOException e) {
- Log.w(TAG, e);
- }
- }
-
- updateToggleButtonState();
- });
-
- disposables.add(disposable);
-
- return future;
- }
-
- private void onSecurityUpdated() {
- Log.i(TAG, "onSecurityUpdated()");
- updateReminders();
- }
-
- private void initializeInsightObserver() {
- }
-
- protected void updateReminders() {
- Context context = getContext();
- if (callback.onUpdateReminders() || context == null) {
- return;
- }
-
- Integer actionableRequestingMembers = groupViewModel.getActionableRequestingMembers().getValue();
- List gv1MigrationSuggestions = groupViewModel.getGroupV1MigrationSuggestions().getValue();
-
- if (ExpiredBuildReminder.isEligible()) {
- reminderView.get().showReminder(new ExpiredBuildReminder(context));
- reminderView.get().setOnActionClickListener(this::handleReminderAction);
- } else if (UnauthorizedReminder.isEligible(context)) {
- reminderView.get().showReminder(new UnauthorizedReminder());
- reminderView.get().setOnActionClickListener(this::handleReminderAction);
- } else if (ServiceOutageReminder.isEligible(context)) {
- ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
- reminderView.get().showReminder(new ServiceOutageReminder());
- } else if (SignalStore.account().isRegistered() &&
- TextSecurePreferences.isShowInviteReminders(context) &&
- !viewModel.isPushAvailable() &&
- !recipient.get().isGroup()) {
- reminderView.get().setOnActionClickListener(this::handleReminderAction);
- } else if (actionableRequestingMembers != null && actionableRequestingMembers > 0) {
- reminderView.get().showReminder(new PendingGroupJoinRequestsReminder(actionableRequestingMembers));
- reminderView.get().setOnActionClickListener(id -> {
- if (id == R.id.reminder_action_review_join_requests) {
- startActivity(ManagePendingAndRequestingMembersActivity.newIntent(context, getRecipient().getGroupId().get().requireV2()));
- }
- });
- } else if (gv1MigrationSuggestions != null && gv1MigrationSuggestions.size() > 0 && recipient.get().isPushV2Group()) {
- reminderView.get().showReminder(new GroupsV1MigrationSuggestionsReminder(gv1MigrationSuggestions));
- reminderView.get().setOnActionClickListener(actionId -> {
- if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
- GroupsV1MigrationSuggestionsDialog.show(requireActivity(), recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
- } else if (actionId == R.id.reminder_action_gv1_suggestion_no_thanks) {
- groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId());
- }
- });
- reminderView.get().setOnDismissListener(() -> {
- });
- } else if (isInBubble() && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29) {
- reminderView.get().showReminder(new BubbleOptOutReminder());
- reminderView.get().setOnActionClickListener(actionId -> {
- SignalStore.tooltips().markBubbleOptOutTooltipSeen();
- reminderView.get().hide();
-
- if (actionId == R.id.reminder_action_bubble_turn_off) {
- Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS)
- .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().getPackageName())
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- }
- });
- } else if (reminderView.resolved()) {
- reminderView.get().hide();
- }
- }
-
- private void handleReminderAction(@IdRes int reminderActionId) {
- if (reminderActionId == R.id.reminder_action_invite) {
- handleInviteLink();
- reminderView.get().requestDismiss();
- } else if (reminderActionId == R.id.reminder_action_update_now) {
- PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
- } else if (reminderActionId == R.id.reminder_action_re_register) {
- startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
- } else {
- throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
- }
- }
-
- private void updateDefaultSubscriptionId(Optional defaultSubscriptionId) {
- Log.i(TAG, "updateDefaultSubscriptionId(" + defaultSubscriptionId.orElse(null) + ")");
- sendButton.setDefaultSubscriptionId(defaultSubscriptionId.orElse(null));
- }
-
- private ListenableFuture initializeIdentityRecords() {
- final SettableFuture future = new SettableFuture<>();
- final Context context = requireContext().getApplicationContext();
-
- if (SignalStore.account().getAci() == null || SignalStore.account().getPni() == null) {
- Log.w(TAG, "Not registered! Skipping initializeIdentityRecords()");
- future.set(false);
- return future;
- }
-
- new AsyncTask>() {
- @Override
- protected @NonNull Pair doInBackground(Recipient... params) {
- List recipients;
-
- if (params[0].isGroup()) {
- recipients = SignalDatabase.groups().getGroupMembers(params[0].requireGroupId(), GroupTable.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
- } else {
- recipients = Collections.singletonList(params[0]);
- }
-
- long startTime = System.currentTimeMillis();
- IdentityRecordList identityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients);
-
- Log.i(TAG, String.format(Locale.US, "Loaded %d identities in %d ms", recipients.size(), System.currentTimeMillis() - startTime));
-
- String message = null;
-
- if (identityRecordList.isUnverified()) {
- message = IdentityUtil.getUnverifiedBannerDescription(context, identityRecordList.getUnverifiedRecipients());
- }
-
- return new Pair<>(identityRecordList, message);
- }
-
- @Override
- protected void onPostExecute(@NonNull Pair result) {
- Log.i(TAG, "Got identity records: " + result.first().isUnverified());
- identityRecords = result.first();
-
- if (result.second() != null) {
- Log.d(TAG, "Replacing banner...");
- unverifiedBannerView.get().display(result.second(), result.first().getUnverifiedRecords(),
- new UnverifiedClickedListener(),
- new UnverifiedDismissedListener());
- } else if (unverifiedBannerView.resolved()) {
- Log.d(TAG, "Clearing banner...");
- unverifiedBannerView.get().hide();
- }
-
- titleView.setVerified(viewModel.isPushAvailable() && identityRecords.isVerified() && !recipient.get().isSelf());
-
- future.set(true);
- }
-
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, recipient.get());
-
- return future;
- }
-
- private void initializeViews(View view) {
- toolbar = view.findViewById(R.id.toolbar);
- toolbarBackground = view.findViewById(R.id.toolbar_background);
- titleView = view.findViewById(R.id.conversation_title_view);
- buttonToggle = view.findViewById(R.id.button_toggle);
- sendButton = view.findViewById(R.id.send_button);
- attachButton = view.findViewById(R.id.attach_button);
- sendEditButton = view.findViewById(R.id.send_edit_button);
- composeText = view.findViewById(R.id.embedded_text_editor);
- charactersLeft = view.findViewById(R.id.space_left);
- emojiDrawerStub = ViewUtil.findStubById(view, R.id.emoji_drawer_stub);
- attachmentKeyboardStub = ViewUtil.findStubById(view, R.id.attachment_keyboard_stub);
- unblockButton = view.findViewById(R.id.unblock_button);
- smsExportStub = ViewUtil.findStubById(view, R.id.sms_export_stub);
- loggedOutStub = ViewUtil.findStubById(view, R.id.logged_out_stub);
- registerButton = view.findViewById(R.id.register_button);
- container = view.findViewById(R.id.layout_container);
- reminderView = ViewUtil.findStubById(view, R.id.reminder_stub);
- unverifiedBannerView = ViewUtil.findStubById(view, R.id.unverified_banner_stub);
- reviewBanner = ViewUtil.findStubById(view, R.id.review_banner_stub);
- quickAttachmentToggle = view.findViewById(R.id.quick_attachment_toggle);
- inlineAttachmentToggle = view.findViewById(R.id.inline_attachment_container);
- inputPanel = view.findViewById(R.id.bottom_panel);
- searchNav = view.findViewById(R.id.conversation_search_nav);
- messageRequestBottomView = view.findViewById(R.id.conversation_activity_message_request_bottom_bar);
- mentionsSuggestions = ViewUtil.findStubById(view, R.id.conversation_mention_suggestions_stub);
- wallpaper = view.findViewById(R.id.conversation_wallpaper);
- wallpaperDim = view.findViewById(R.id.conversation_wallpaper_dim);
- voiceNotePlayerViewStub = ViewUtil.findStubById(view, R.id.voice_note_player_stub);
- navigationBarBackground = view.findViewById(R.id.navbar_background);
- scheduledMessagesBarStub = ViewUtil.findStubById(view, R.id.scheduled_messages_stub);
-
- ImageButton quickCameraToggle = view.findViewById(R.id.quick_camera_toggle);
- ImageButton inlineAttachmentButton = view.findViewById(R.id.inline_attachment_button);
-
- Stub reactionOverlayStub = ViewUtil.findStubById(view, R.id.conversation_reaction_scrubber_stub);
- reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
-
- noLongerMemberBanner = view.findViewById(R.id.conversation_no_longer_member_banner);
- cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(view, R.id.conversation_cannot_send_announcement_stub);
- requestingMemberBanner = view.findViewById(R.id.conversation_requesting_banner);
- cancelJoinRequest = view.findViewById(R.id.conversation_cancel_request);
- releaseChannelUnmute = ViewUtil.findStubById(view, R.id.conversation_release_notes_unmute_stub);
- joinGroupCallButton = view.findViewById(R.id.conversation_group_call_join);
-
- sendButton.setPopupContainer((ViewGroup) view);
- sendButton.setSnackbarContainer(view.findViewById(R.id.fragment_content));
-
- container.setIsBubble(isInBubble());
- container.addOnKeyboardShownListener(this);
- inputPanel.setListener(this);
- inputPanel.setMediaListener(this);
-
- attachmentManager = new AttachmentManager(requireContext(), view, this);
- audioRecorder = new AudioRecorder(requireContext(), inputPanel);
- typingTextWatcher = new ComposeTextWatcher();
-
- SendButtonListener sendButtonListener = new SendButtonListener();
- ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
-
- composeText.setOnEditorActionListener(sendButtonListener);
- composeText.setCursorPositionChangedListener(this);
- attachButton.setOnClickListener(new AttachButtonListener());
- attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
- sendButton.setOnClickListener(sendButtonListener);
- sendEditButton.setOnClickListener(v -> handleSendEditMessage());
- sendButton.setScheduledSendListener(new SendButton.ScheduledSendListener() {
- @Override
- public void onSendScheduled() {
- ScheduleMessageContextMenu.show(sendButton, (ViewGroup) requireView(), time -> {
- if (time == -1) {
- ScheduleMessageTimePickerBottomSheet.showSchedule(getChildFragmentManager());
- } else {
- sendMessage(null, time);
- }
- return Unit.INSTANCE;
- });
- }
-
- @Override
- public boolean canSchedule() {
- return !(inputPanel.isRecordingInLockedMode() || draftViewModel.getVoiceNoteDraft() != null);
- }
- });
- sendButton.setEnabled(true);
- sendButton.addOnSelectionChangedListener((newMessageSendType, manuallySelected) -> {
- if (getContext() == null) {
- Log.w(TAG, "onSelectionChanged called in detached state. Ignoring.");
- return;
- }
-
- calculateCharactersRemaining();
- updateLinkPreviewState();
- linkPreviewViewModel.onTransportChanged(newMessageSendType.usesSmsTransport());
- composeText.setMessageSendType(newMessageSendType);
-
- updateSendButtonColor(newMessageSendType);
- });
-
- titleView.setOnStoryRingClickListener(v -> handleStoryRingClick());
- titleView.setOnClickListener(v -> handleConversationSettings());
- titleView.setOnLongClickListener(v -> handleDisplayQuickContact());
- unblockButton.setOnClickListener(v -> handleUnblock());
- registerButton.setOnClickListener(v -> handleRegisterForSignal());
-
- composeText.setOnKeyListener(composeKeyPressedListener);
- composeText.addTextChangedListener(composeKeyPressedListener);
- composeText.setOnEditorActionListener(sendButtonListener);
- composeText.setOnClickListener(composeKeyPressedListener);
- composeText.setOnFocusChangeListener(composeKeyPressedListener);
-
- searchNav.setEventListener(this);
-
- inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
-
- reactionDelegate.setOnReactionSelectedListener(this);
-
- joinGroupCallButton.setOnClickListener(v -> handleVideo());
-
- voiceNoteMediaController.getVoiceNotePlayerViewState().observe(getViewLifecycleOwner(), state -> {
- if (state.isPresent()) {
- requireVoiceNotePlayerView().show();
- requireVoiceNotePlayerView().setState(state.get());
- } else if (voiceNotePlayerViewStub.resolved()) {
- requireVoiceNotePlayerView().hide();
- }
- });
-
- voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), inputPanel.getPlaybackStateObserver());
-
- material3OnScrollHelper = new Material3OnScrollHelper(requireActivity(), Collections.singletonList(toolbarBackground), Collections.emptyList(), getViewLifecycleOwner()) {
- @Override
- public @NonNull ColorSet getActiveColorSet() {
- return new ColorSet(getActiveToolbarColor(wallpaper.getDrawable() != null));
- }
-
- @Override
- public @NonNull ColorSet getInactiveColorSet() {
- return new ColorSet(getInactiveToolbarColor(wallpaper.getDrawable() != null));
- }
- };
- }
-
- private void updateSendButtonColor(MessageSendType newMessageSendType) {
- buttonToggle.getBackground().setColorFilter(getSendButtonColor(newMessageSendType), PorterDuff.Mode.MULTIPLY);
- buttonToggle.getBackground().invalidateSelf();
- }
-
- private @ColorInt int getSendButtonColor(MessageSendType newTransport) {
- if (newTransport.usesSmsTransport()) {
- return getResources().getColor(newTransport.getBackgroundColorRes());
- } else if (recipient != null) {
- return getRecipient().getChatColors().asSingleColor();
- } else {
- return getResources().getColor(newTransport.getBackgroundColorRes());
- }
- }
-
- private @NonNull VoiceNotePlayerView requireVoiceNotePlayerView() {
- if (voiceNotePlayerView == null) {
- voiceNotePlayerView = voiceNotePlayerViewStub.get().findViewById(R.id.voice_note_player_view);
- voiceNotePlayerView.setListener(new VoiceNotePlayerViewListener());
- }
-
- return voiceNotePlayerView;
- }
-
- private void updateWallpaper(@Nullable ChatWallpaper chatWallpaper) {
- Log.d(TAG, "Setting wallpaper.");
- if (chatWallpaper != null) {
- chatWallpaper.loadInto(wallpaper);
- ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(wallpaperDim, chatWallpaper);
- inputPanel.setWallpaperEnabled(true);
- if (attachmentKeyboardStub.resolved()) {
- attachmentKeyboardStub.get().setWallpaperEnabled(true);
- }
-
- material3OnScrollHelper.setColorImmediate();
- int toolbarTextAndIconColor = getResources().getColor(R.color.signal_colorNeutralInverse);
- toolbar.setTitleTextColor(toolbarTextAndIconColor);
- setToolbarActionItemTint(toolbar, toolbarTextAndIconColor);
- if (!smsExportStub.resolved()) {
- WindowUtil.setNavigationBarColor(requireActivity(), getResources().getColor(R.color.conversation_navigation_wallpaper));
- }
- } else {
- wallpaper.setImageDrawable(null);
- wallpaperDim.setVisibility(View.GONE);
- inputPanel.setWallpaperEnabled(false);
- if (attachmentKeyboardStub.resolved()) {
- attachmentKeyboardStub.get().setWallpaperEnabled(false);
- }
-
- material3OnScrollHelper.setColorImmediate();
- int toolbarTextAndIconColor = getResources().getColor(R.color.signal_colorOnSurface);
- toolbar.setTitleTextColor(toolbarTextAndIconColor);
- setToolbarActionItemTint(toolbar, toolbarTextAndIconColor);
- if (!releaseChannelUnmute.resolved() && !smsExportStub.resolved()) {
- WindowUtil.setNavigationBarColor(requireActivity(), getResources().getColor(R.color.signal_colorBackground));
- }
- }
- fragment.onWallpaperChanged(chatWallpaper);
- messageRequestBottomView.setWallpaperEnabled(chatWallpaper != null);
- }
-
- private static @ColorRes int getActiveToolbarColor(boolean hasWallpaper) {
- return hasWallpaper ? R.color.conversation_toolbar_color_wallpaper_scrolled
- : R.color.signal_colorSurface2;
- }
-
- private static @ColorRes int getInactiveToolbarColor(boolean hasWallpaper) {
- return hasWallpaper ? R.color.conversation_toolbar_color_wallpaper
- : R.color.signal_colorBackground;
- }
-
- private void setToolbarActionItemTint(@NonNull Toolbar toolbar, @ColorInt int tint) {
- for (int i = 0; i < toolbar.getMenu().size(); i++) {
- MenuItem menuItem = toolbar.getMenu().getItem(i);
- MenuItemCompat.setIconTintList(menuItem, ColorStateList.valueOf(tint));
- }
-
- if (toolbar.getNavigationIcon() != null) {
- toolbar.getNavigationIcon().setColorFilter(new SimpleColorFilter(tint));
- }
-
- if (toolbar.getOverflowIcon() != null) {
- toolbar.getOverflowIcon().setColorFilter(new SimpleColorFilter(tint));
- }
- }
-
- protected void initializeActionBar() {
- toolbar.addMenuProvider(new ConversationOptionsMenu.Provider(this, disposables, true));
- invalidateOptionsMenu();
- toolbar.setNavigationContentDescription(R.string.ConversationFragment__content_description_back_button);
- if (isInBubble()) {
- toolbar.setNavigationIcon(DrawableUtil.tint(ContextUtil.requireDrawable(requireContext(), R.drawable.ic_notification),
- ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)));
- toolbar.setNavigationOnClickListener(unused -> startActivity(MainActivity.clearTop(requireContext())));
- }
-
- callback.onInitializeToolbar(toolbar);
- }
-
- @Override
- public boolean isInBubble() {
- return callback.isInBubble();
- }
-
- private void initializeResources(@NonNull ConversationIntents.Args args) {
- if (recipient != null) {
- recipient.removeObservers(this);
- }
-
- recipient = Recipient.live(args.getRecipientId());
- threadId = args.getThreadId();
- distributionType = args.getDistributionType();
- glideRequests = GlideApp.with(this);
-
- Log.i(TAG, "[initializeResources] Recipient: " + recipient.getId() + ", Thread: " + threadId);
-
- disposables.add(
- recipient
- .observable()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(this::onRecipientChanged)
- );
- }
-
- private void initializeLinkPreviewObserver() {
- linkPreviewViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) new LinkPreviewViewModel.Factory(new LinkPreviewRepository())).get(LinkPreviewViewModel.class);
-
- linkPreviewViewModel.getLinkPreviewState().observe(getViewLifecycleOwner(), previewState -> {
- if (previewState == null) return;
-
- if (previewState.isLoading()) {
- inputPanel.setLinkPreviewLoading();
- } else if (previewState.hasLinks() && !previewState.getLinkPreview().isPresent()) {
- inputPanel.setLinkPreviewNoPreview(previewState.getError());
- } else {
- inputPanel.setLinkPreview(glideRequests, previewState.getLinkPreview());
- }
-
- updateToggleButtonState();
- });
- }
-
- private void initializeSearchObserver() {
- ConversationSearchViewModel.Factory viewModelFactory = new ConversationSearchViewModel.Factory(getString(R.string.note_to_self));
-
- searchViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) viewModelFactory).get(ConversationSearchViewModel.class);
-
- searchViewModel.getSearchResults().observe(getViewLifecycleOwner(), result -> {
- if (result == null) return;
-
- if (!result.getResults().isEmpty()) {
- MessageResult messageResult = result.getResults().get(result.getPosition());
- fragment.jumpToMessage(messageResult.getMessageRecipient().getId(), messageResult.getReceivedTimestampMs(), searchViewModel::onMissingResult);
- }
-
- searchNav.setData(result.getPosition(), result.getResults().size());
- });
- }
-
- private void initializeStickerObserver() {
- StickerSearchRepository repository = new StickerSearchRepository();
-
- stickerViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) new ConversationStickerViewModel.Factory(requireActivity().getApplication(), repository))
- .get(ConversationStickerViewModel.class);
-
- stickerViewModel.getStickerResults().observe(getViewLifecycleOwner(), stickers -> {
- if (stickers == null) return;
-
- inputPanel.setStickerSuggestions(stickers);
- });
-
- stickerViewModel.getStickersAvailability().observe(getViewLifecycleOwner(), stickersAvailable -> {
- if (stickersAvailable == null) return;
-
- boolean isSystemEmojiPreferred = SignalStore.settings().isPreferSystemEmoji();
- MediaKeyboardMode keyboardMode = TextSecurePreferences.getMediaKeyboardMode(requireContext());
- boolean stickerIntro = !TextSecurePreferences.hasSeenStickerIntroTooltip(requireContext());
-
- if (stickersAvailable) {
- inputPanel.showMediaKeyboardToggle(true);
- switch (keyboardMode) {
- case EMOJI:
- inputPanel.setMediaKeyboardToggleMode(isSystemEmojiPreferred ? KeyboardPage.STICKER : KeyboardPage.EMOJI);
- break;
- case STICKER:
- inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
- break;
- case GIF:
- inputPanel.setMediaKeyboardToggleMode(KeyboardPage.GIF);
- break;
- }
- if (stickerIntro) showStickerIntroductionTooltip();
- }
-
- if (emojiDrawerStub.resolved()) {
- initializeMediaKeyboardProviders();
- }
- });
- }
-
- private void initializeViewModel(@NonNull ConversationIntents.Args args) {
- this.viewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) new ConversationViewModel.Factory()).get(ConversationViewModel.class);
-
- this.viewModel.setArgs(args);
- this.viewModel.getEvents().observe(getViewLifecycleOwner(), this::onViewModelEvent);
- disposables.add(this.viewModel.getWallpaper().subscribe(w -> updateWallpaper(w.orElse(null))));
- }
-
- private void initializeGroupViewModel() {
- groupViewModel = new ViewModelProvider(this, (ViewModelProvider.Factory) new ConversationGroupViewModel.Factory()).get(ConversationGroupViewModel.class);
- recipient.observe(this, groupViewModel::onRecipientChange);
- groupViewModel.getGroupActiveState().observe(getViewLifecycleOwner(), unused -> invalidateOptionsMenu());
- groupViewModel.getReviewState().observe(getViewLifecycleOwner(), this::presentGroupReviewBanner);
- }
-
- private void initializeMentionsViewModel() {
- mentionsViewModel = new ViewModelProvider(requireActivity(), new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
- inlineQueryViewModel = new ViewModelProvider(requireActivity()).get(InlineQueryViewModel.class);
-
- inlineQueryResultsController = new InlineQueryResultsController(
- inlineQueryViewModel,
- inputPanel,
- (ViewGroup) requireView(),
- composeText,
- getViewLifecycleOwner()
- );
- inlineQueryResultsController.onOrientationChange(getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE);
-
- recipient.observe(getViewLifecycleOwner(), r -> {
- if (r.isPushV2Group() && !mentionsSuggestions.resolved()) {
- mentionsSuggestions.get();
- }
- mentionsViewModel.onRecipientChange(r);
- });
-
- composeText.setInlineQueryChangedListener(new InlineQueryChangedListener() {
- @Override
- public void onQueryChanged(@NonNull InlineQuery inlineQuery) {
- if (inlineQuery instanceof InlineQuery.Mention) {
- if (getRecipient().isPushV2Group() && getRecipient().isActiveGroup()) {
- if (!mentionsSuggestions.resolved()) {
- mentionsSuggestions.get();
- }
- mentionsViewModel.onQueryChange(inlineQuery.getQuery());
- }
- inlineQueryViewModel.onQueryChange(inlineQuery);
- } else if (inlineQuery instanceof InlineQuery.Emoji) {
- inlineQueryViewModel.onQueryChange(inlineQuery);
- mentionsViewModel.onQueryChange(null);
- } else if (inlineQuery instanceof InlineQuery.NoQuery) {
- mentionsViewModel.onQueryChange(null);
- inlineQueryViewModel.onQueryChange(inlineQuery);
- }
- }
-
- @Override
- public void clearQuery() {
- onQueryChanged(InlineQuery.NoQuery.INSTANCE);
- }
- });
-
- composeText.setMentionValidator(annotations -> {
- if (!getRecipient().isPushV2Group() || !getRecipient().isActiveGroup()) {
- return annotations;
- }
-
- Set validRecipientIds = Stream.of(getRecipient().getParticipantIds())
- .map(id -> MentionAnnotation.idToMentionAnnotationValue(id))
- .collect(Collectors.toSet());
-
- return Stream.of(annotations)
- .filterNot(a -> validRecipientIds.contains(a.getValue()))
- .toList();
- });
-
- mentionsViewModel.getSelectedRecipient().observe(getViewLifecycleOwner(), recipient -> {
- composeText.replaceTextWithMention(recipient.getDisplayName(requireContext()), recipient.getId());
- });
-
- Disposable disposable = inlineQueryViewModel
- .getSelection()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(r -> {
- composeText.replaceText(r);
- });
-
- disposables.add(disposable);
- }
-
- public void initializeGroupCallViewModel() {
- groupCallViewModel = new ViewModelProvider(this, new GroupCallViewModel.Factory()).get(GroupCallViewModel.class);
-
- recipient.observe(this, r -> {
- groupCallViewModel.onRecipientChange(r);
- });
-
- groupCallViewModel.hasActiveGroupCall().observe(getViewLifecycleOwner(), hasActiveCall -> {
- invalidateOptionsMenu();
- joinGroupCallButton.setVisibility(hasActiveCall ? View.VISIBLE : View.GONE);
- });
-
- groupCallViewModel.groupCallHasCapacity().observe(getViewLifecycleOwner(), hasCapacity -> joinGroupCallButton.setText(hasCapacity ? R.string.ConversationActivity_join : R.string.ConversationActivity_full));
- }
-
- public void initializeDraftViewModel() {
- draftViewModel = new ViewModelProvider(this).get(DraftViewModel.class);
-
- recipient.observe(getViewLifecycleOwner(), r -> {
- draftViewModel.onRecipientChanged(r);
- });
-
- draftViewModel.setThreadId(threadId);
- draftViewModel.setDistributionType(distributionType);
-
- disposables.add(
- draftViewModel
- .getState()
- .distinctUntilChanged(state -> state.getVoiceNoteDraft())
- .subscribe(state -> {
- inputPanel.setVoiceNoteDraft(state.getVoiceNoteDraft());
- updateToggleButtonState();
- })
- );
- }
-
- @Override
- public void showGroupCallingTooltip() {
- if (!SignalStore.tooltips().shouldShowGroupCallingTooltip() || callingTooltipShown) {
- return;
- }
-
- View anchor = requireView().findViewById(R.id.menu_video_secure);
- if (anchor == null) {
- Log.w(TAG, "Video Call tooltip anchor is null. Skipping tooltip...");
- return;
- }
-
- callingTooltipShown = true;
-
- SignalStore.tooltips().markGroupCallSpeakerViewSeen();
- TooltipPopup.forTarget(anchor)
- .setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_accent_green))
- .setTextColor(getResources().getColor(R.color.core_white))
- .setText(R.string.ConversationActivity__tap_here_to_start_a_group_call)
- .setOnDismissListener(() -> SignalStore.tooltips().markGroupCallingTooltipSeen())
- .show(TooltipPopup.POSITION_BELOW);
- }
-
- @Override
- public void handleFormatText(@IdRes int id) {
- composeText.handleFormatText(id);
- }
-
- private void showStickerIntroductionTooltip() {
- TextSecurePreferences.setMediaKeyboardMode(requireContext(), MediaKeyboardMode.STICKER);
- inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
-
- TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
- .setBackgroundTint(getResources().getColor(R.color.core_ultramarine))
- .setTextColor(getResources().getColor(R.color.core_white))
- .setText(R.string.ConversationActivity_new_say_it_with_stickers)
- .setOnDismissListener(() -> {
- TextSecurePreferences.setHasSeenStickerIntroTooltip(requireContext(), true);
- EventBus.getDefault().removeStickyEvent(StickerPackInstallEvent.class);
- })
- .show(TooltipPopup.POSITION_ABOVE);
- }
-
- @Override
- public void onReactionSelected(MessageRecord messageRecord, String emoji) {
- final Context context = requireContext().getApplicationContext();
-
- reactionDelegate.hide();
-
- SignalExecutors.BOUNDED.execute(() -> {
- ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
- .filter(record -> record.getAuthor().equals(Recipient.self().getId()))
- .findFirst()
- .orElse(null);
-
- if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
- MessageSender.sendReactionRemoval(context, new MessageId(messageRecord.getId()), oldRecord);
- } else {
- MessageSender.sendNewReaction(context, new MessageId(messageRecord.getId()), emoji);
- }
- });
- }
-
- @Override
- public void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji) {
- ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
- .filter(record -> record.getAuthor().equals(Recipient.self().getId()))
- .findFirst()
- .orElse(null);
-
- if (oldRecord != null && hasAddedCustomEmoji) {
- final Context context = requireContext().getApplicationContext();
-
- reactionDelegate.hide();
-
- SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context,
- new MessageId(messageRecord.getId()),
- oldRecord));
- } else {
- reactionDelegate.hideForReactWithAny();
-
- ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage)
- .show(getChildFragmentManager(), "BOTTOM");
- }
- }
-
- @Override
- public void onReactWithAnyEmojiDialogDismissed() {
- reactionDelegate.hide();
- }
-
- @Override
- public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
- reactionDelegate.hide();
- }
-
- @Override
- public void onSearchMoveUpPressed() {
- searchViewModel.onMoveUp();
- }
-
- @Override
- public void onSearchMoveDownPressed() {
- searchViewModel.onMoveDown();
- }
-
- private void initializeProfiles() {
- if (!viewModel.isPushAvailable()) {
- Log.i(TAG, "SMS contact, no profile fetch needed.");
- return;
- }
-
- RetrieveProfileJob.enqueueAsync(recipient.getId());
- }
-
- private void initializeGv1Migration() {
- GroupV1MigrationJob.enqueuePossibleAutoMigrate(recipient.getId());
- }
-
- private void onRecipientChanged(@NonNull Recipient recipient) {
- if (getContext() == null) {
- Log.w(TAG, "onRecipientChanged called in detached state. Ignoring.");
- return;
- }
-
- Log.i(TAG, "onModified(" + recipient.getId() + ") " + recipient.getRegistered());
- titleView.setTitle(glideRequests, recipient);
- titleView.setVerified(identityRecords.isVerified() && !recipient.isSelf());
- setBlockedUserState(recipient, viewModel.getConversationStateSnapshot().getSecurityInfo());
- updateReminders();
- updatePaymentsAvailable();
- updateSendButtonColor(sendButton.getSelectedSendType());
-
- if (searchViewItem == null || !searchViewItem.isActionViewExpanded()) {
- invalidateOptionsMenu();
- }
-
- if (groupViewModel != null) {
- groupViewModel.onRecipientChange(recipient);
- }
-
- if (mentionsViewModel != null) {
- mentionsViewModel.onRecipientChange(recipient);
- }
-
- if (groupCallViewModel != null) {
- groupCallViewModel.onRecipientChange(recipient);
- }
-
- if (draftViewModel != null) {
- draftViewModel.onRecipientChanged(recipient);
- }
-
- if (this.threadId == -1) {
- SimpleTask.run(() -> SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId()), threadId -> {
- if (this.threadId != threadId) {
- Log.d(TAG, "Thread id changed via recipient change");
- this.threadId = threadId;
- fragment.reload(recipient, this.threadId);
- setVisibleThread(this.threadId);
- draftViewModel.setThreadId(this.threadId);
- }
- });
- }
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onIdentityRecordUpdate(final IdentityRecord event) {
- initializeIdentityRecords();
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
- public void onStickerPackInstalled(final StickerPackInstallEvent event) {
- if (!TextSecurePreferences.hasSeenStickerIntroTooltip(requireContext())) return;
-
- EventBus.getDefault().removeStickyEvent(event);
-
- if (!inputPanel.isStickerMode()) {
- TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
- .setText(R.string.ConversationActivity_sticker_pack_installed)
- .setIconGlideModel(event.getIconGlideModel())
- .show(TooltipPopup.POSITION_ABOVE);
- }
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
- public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent event) {
- if (groupCallViewModel != null) {
- groupCallViewModel.onGroupCallPeekEvent(event);
- }
- }
-
- private void initializeReceivers() {
- securityUpdateReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- viewModel.updateSecurityInfo();
- calculateCharactersRemaining();
- }
- };
-
- requireActivity().registerReceiver(securityUpdateReceiver,
- new IntentFilter(SecurityEvent.SECURITY_UPDATE_EVENT),
- KeyCachingService.KEY_PERMISSION, null);
- }
-
- //////// Helper Methods
-
- private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) {
- return setMedia(uri, mediaType, 0, 0, false, false);
- }
-
- private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height, boolean borderless, boolean videoGif) {
- if (uri == null) {
- return new SettableFuture<>(false);
- }
-
- if (MediaType.VCARD.equals(mediaType) && viewModel.isPushAvailable()) {
- openContactShareEditor(uri);
- return new SettableFuture<>(false);
- } else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
- String mimeType = MediaUtil.getMimeType(requireContext(), uri);
- if (mimeType == null) {
- mimeType = mediaType.toFallbackMimeType();
- }
-
- Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, videoGif, Optional.empty(), Optional.empty(), Optional.empty());
- startActivityForResult(MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
- return new SettableFuture<>(false);
- } else {
- return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
- }
- }
-
- private void openContactShareEditor(Uri contactUri) {
- Intent intent = ContactShareEditActivity.getIntent(requireContext(), Collections.singletonList(contactUri), getSendButtonColor(sendButton.getSelectedSendType()));
- startActivityForResult(intent, GET_CONTACT_DETAILS);
- }
-
- private void addAttachmentContactInfo(Uri contactUri) {
- ContactAccessor contactDataList = ContactAccessor.getInstance();
- ContactData contactData = contactDataList.getContactData(requireContext(), contactUri);
-
- if (contactData.numbers.size() == 1) composeText.append(contactData.numbers.get(0).number);
- else if (contactData.numbers.size() > 1) selectContactInfo(contactData);
- }
-
- private void sendSharedContact(List contacts) {
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- boolean initiating = threadId == -1;
-
- sendMediaMessage(recipient.getId(), sendButton.getSelectedSendType(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, false, null);
- }
-
- private void selectContactInfo(ContactData contactData) {
- final CharSequence[] numbers = new CharSequence[contactData.numbers.size()];
- final CharSequence[] numberItems = new CharSequence[contactData.numbers.size()];
-
- for (int i = 0; i < contactData.numbers.size(); i++) {
- numbers[i] = contactData.numbers.get(i).number;
- numberItems[i] = contactData.numbers.get(i).type + ": " + contactData.numbers.get(i).number;
- }
-
- AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireContext());
- builder.setIcon(R.drawable.ic_account_box);
- builder.setTitle(R.string.ConversationActivity_select_contact_info);
-
- builder.setItems(numberItems, (dialog, which) -> composeText.append(numbers[which]));
- builder.show();
- }
-
- private void setBlockedUserState(Recipient recipient, @NonNull ConversationSecurityInfo conversationSecurityInfo) {
- if (!conversationSecurityInfo.isInitialized()) {
- Log.i(TAG, "Ignoring blocked state update for uninitialized security info.");
- return;
- }
-
- if (conversationSecurityInfo.isClientExpired() || conversationSecurityInfo.isUnauthorized()) {
- unblockButton.setVisibility(View.GONE);
- inputPanel.setHideForBlockedState(true);
- smsExportStub.setVisibility(View.GONE);
- registerButton.setVisibility(View.GONE);
- loggedOutStub.setVisibility(View.VISIBLE);
- messageRequestBottomView.setVisibility(View.GONE);
-
- int color = ContextCompat.getColor(requireContext(), recipient.hasWallpaper() ? R.color.wallpaper_bubble_color : R.color.signal_colorBackground);
- loggedOutStub.get().setBackgroundColor(color);
- WindowUtil.setNavigationBarColor(requireActivity(), color);
-
- TextView message = loggedOutStub.get().findViewById(R.id.logged_out_message);
- MaterialButton actionButton = loggedOutStub.get().findViewById(R.id.logged_out_button);
-
- if (conversationSecurityInfo.isClientExpired()) {
- message.setText(R.string.ExpiredBuildReminder_this_version_of_signal_has_expired);
- actionButton.setText(R.string.ConversationFragment__update_build);
- actionButton.setOnClickListener(v -> {
- PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
- });
- } else if (conversationSecurityInfo.isUnauthorized()) {
- message.setText(R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device);
- actionButton.setText(R.string.ConversationFragment__reregister_signal);
- actionButton.setOnClickListener(v -> {
- startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()));
- });
- }
- } else if (!conversationSecurityInfo.isPushAvailable() && isPushGroupConversation()) {
- unblockButton.setVisibility(View.GONE);
- inputPanel.setHideForBlockedState(true);
- smsExportStub.setVisibility(View.GONE);
- loggedOutStub.setVisibility(View.GONE);
- registerButton.setVisibility(View.VISIBLE);
- } else if (!conversationSecurityInfo.isPushAvailable() && !(SignalStore.misc().getSmsExportPhase().isSmsSupported() && conversationSecurityInfo.isDefaultSmsApplication()) && (recipient.hasSmsAddress() || recipient.isMmsGroup())) {
- unblockButton.setVisibility(View.GONE);
- inputPanel.setHideForBlockedState(true);
- smsExportStub.setVisibility(View.VISIBLE);
- registerButton.setVisibility(View.GONE);
- loggedOutStub.setVisibility(View.GONE);
-
- int color = ContextCompat.getColor(requireContext(), recipient.hasWallpaper() ? R.color.wallpaper_bubble_color : R.color.signal_colorBackground);
- smsExportStub.get().setBackgroundColor(color);
- WindowUtil.setNavigationBarColor(requireActivity(), color);
-
- TextView message = smsExportStub.get().findViewById(R.id.export_sms_message);
- MaterialButton actionButton = smsExportStub.get().findViewById(R.id.export_sms_button);
-
- if (conversationSecurityInfo.getHasUnexportedInsecureMessages()) {
- message.setText(R.string.ConversationActivity__sms_messaging_is_no_longer_supported_in_signal_you_can_export_your_messages_to_another_app_on_your_phone);
- actionButton.setText(R.string.ConversationActivity__export_sms_messages);
- actionButton.setOnClickListener(v -> startActivity(SmsExportActivity.createIntent(requireContext())));
- } else {
- message.setText(requireContext().getString(R.string.ConversationActivity__sms_messaging_is_no_longer_supported_in_signal_invite_s_to_to_signal_to_keep_the_conversation_here,
- recipient.getDisplayName(requireContext())));
- actionButton.setText(R.string.ConversationActivity__invite_to_signal);
- actionButton.setOnClickListener(v -> handleInviteLink());
- }
- } else if (recipient.isReleaseNotes() && !recipient.isBlocked()) {
- unblockButton.setVisibility(View.GONE);
- inputPanel.setHideForBlockedState(true);
- smsExportStub.setVisibility(View.GONE);
- registerButton.setVisibility(View.GONE);
-
- if (recipient.isMuted()) {
- View unmuteBanner = releaseChannelUnmute.get();
- unmuteBanner.setVisibility(View.VISIBLE);
- unmuteBanner.findViewById(R.id.conversation_activity_unmute_button)
- .setOnClickListener(v -> handleUnmuteNotifications());
- WindowUtil.setNavigationBarColor(requireActivity(), getResources().getColor(R.color.signal_colorSurface2));
- } else if (releaseChannelUnmute.resolved()) {
- releaseChannelUnmute.get().setVisibility(View.GONE);
- WindowUtil.setNavigationBarColor(requireActivity(), getResources().getColor(R.color.signal_colorBackground));
- }
- } else {
- boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
- inputPanel.setHideForBlockedState(inactivePushGroup);
- unblockButton.setVisibility(View.GONE);
- smsExportStub.setVisibility(View.GONE);
- registerButton.setVisibility(View.GONE);
- }
-
- if (releaseChannelUnmute.resolved() && !recipient.isReleaseNotes()) {
- releaseChannelUnmute.get().setVisibility(View.GONE);
- }
- }
-
- private void calculateCharactersRemaining() {
- String messageBody = composeText.getTextTrimmed().toString();
- MessageSendType sendType = sendButton.getSelectedSendType();
- CharacterState characterState = sendType.calculateCharacters(messageBody);
-
- if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
- charactersLeft.setText(String.format(Locale.getDefault(),
- "%d/%d (%d)",
- characterState.charactersRemaining,
- characterState.maxTotalMessageSize,
- characterState.messagesSpent));
- charactersLeft.setVisibility(View.VISIBLE);
- } else {
- charactersLeft.setVisibility(View.GONE);
- }
- }
-
- private void initializeMediaKeyboardProviders() {
- keyboardPagerViewModel = new ViewModelProvider(requireActivity()).get(KeyboardPagerViewModel.class);
-
- switch (TextSecurePreferences.getMediaKeyboardMode(requireContext())) {
- case EMOJI:
- keyboardPagerViewModel.switchToPage(KeyboardPage.EMOJI);
- break;
- case STICKER:
- keyboardPagerViewModel.switchToPage(KeyboardPage.STICKER);
- break;
- case GIF:
- keyboardPagerViewModel.switchToPage(KeyboardPage.GIF);
- break;
- }
- }
-
- public boolean isInMessageRequest() {
- return messageRequestBottomView.getVisibility() == View.VISIBLE;
- }
-
- private boolean isActiveGroup() {
- if (!isGroupConversation()) return false;
-
- Optional record = SignalDatabase.groups().getGroup(getRecipient().getId());
- return record.isPresent() && record.get().isActive();
- }
-
- private boolean isGroupConversation() {
- return getRecipient() != null && getRecipient().isGroup();
- }
-
- private boolean isPushGroupConversation() {
- return getRecipient() != null && getRecipient().isPushGroup();
- }
-
- private boolean isPushGroupV1Conversation() {
- return getRecipient() != null && getRecipient().isPushV1Group();
- }
-
- private boolean isSmsForced() {
- return sendButton.isManualSelection() && sendButton.getSelectedSendType().usesSmsTransport();
- }
-
- protected Recipient getRecipient() {
- return this.recipient.get();
- }
-
- private String getMessage() throws InvalidMessageException {
- String rawText = composeText.getTextTrimmed().toString();
-
- if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
- throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation));
-
- return rawText;
- }
-
- private MediaConstraints getCurrentMediaConstraints() {
- return sendButton.getSelectedSendType().usesSignalTransport()
- ? MediaConstraints.getPushMediaConstraints()
- : MediaConstraints.getMmsMediaConstraints(sendButton.getSelectedSendType().getSimSubscriptionIdOr(-1));
- }
-
- private void markLastSeen() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Long... params) {
- SignalDatabase.threads().setLastSeen(params[0]);
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
- }
-
- protected void sendComplete(long threadId) {
- boolean refreshFragment = (threadId != this.threadId);
- this.threadId = threadId;
-
- if (fragment == null || !fragment.isVisible() || requireActivity().isFinishing()) {
- callback.onSendComplete(threadId);
- return;
- }
-
- fragment.setLastSeen(0);
-
- if (refreshFragment) {
- fragment.reload(recipient.get(), threadId);
- setVisibleThread(threadId);
- }
- if (!inputPanel.inEditMessageMode()) {
- fragment.scrollToBottom();
- }
- attachmentManager.cleanup();
-
- updateLinkPreviewState();
- callback.onSendComplete(threadId);
-
- draftViewModel.onSendComplete(threadId);
-
- inputPanel.exitEditMessageMode();
- }
-
- private void sendMessage(@Nullable String metricId) {
- sendMessage(metricId, -1);
- }
-
- private void sendMessage(@Nullable String metricId, long scheduledDate) {
- if (scheduledDate != -1 && ReenableScheduledMessagesDialogFragment.showIfNeeded(requireContext(), getChildFragmentManager(), metricId, scheduledDate)) {
- return;
- }
-
- if (inputPanel.isRecordingInLockedMode()) {
- inputPanel.releaseRecordingLock();
- return;
- }
-
- Draft voiceNote = draftViewModel.getVoiceNoteDraft();
- if (voiceNote != null) {
- AudioSlide audioSlide = AudioSlide.createFromVoiceNoteDraft(requireContext(), voiceNote);
-
- sendVoiceNote(Objects.requireNonNull(audioSlide.getUri()), audioSlide.getFileSize(), scheduledDate);
- return;
- }
-
- try {
- Recipient recipient = getRecipient();
-
- if (recipient == null) {
- throw new RecipientFormattingException("Badly formatted");
- }
-
- String message = getMessage();
- MessageSendType sendType = sendButton.getSelectedSendType();
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
- boolean initiating = threadId == -1;
- boolean isEditMessage = inputPanel.inEditMessageMode();
- boolean needsSplit = !sendType.usesSmsTransport() && message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
- boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
- recipient.isGroup() ||
- recipient.getEmail().isPresent() ||
- inputPanel.getQuote().isPresent() ||
- composeText.hasMentions() ||
- composeText.hasStyling() ||
- linkPreviewViewModel.hasLinkPreview() ||
- needsSplit;
-
- Log.i(TAG, "[sendMessage] recipient: " + recipient.getId() + ", threadId: " + threadId + ", sendType: " + (sendType.usesSignalTransport() ? "signal" : "sms") + ", isManual: " + sendButton.isManualSelection());
-
- if (!sendType.usesSignalTransport() && isEditMessage) {
- Toast.makeText(requireContext(),
- R.string.ConversationActivity_edit_sms_message_error,
- Toast.LENGTH_LONG)
- .show();
- } else if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !viewModel.getConversationStateSnapshot().isMmsEnabled()) {
- handleManualMmsRequired();
- } else if (sendType.usesSignalTransport() && (identityRecords.isUnverified(true) || identityRecords.isUntrusted(true))) {
- handleRecentSafetyNumberChange();
- } else if (isMediaMessage) {
- sendMediaMessage(sendType, expiresIn, false, initiating, metricId, scheduledDate, inputPanel.getEditMessageId());
- } else {
- sendTextMessage(sendType, expiresIn, initiating, metricId, scheduledDate, inputPanel.getEditMessageId());
- }
- } catch (RecipientFormattingException ex) {
- Toast.makeText(requireContext(),
- R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation,
- Toast.LENGTH_LONG).show();
- Log.w(TAG, ex);
- } catch (InvalidMessageException ex) {
- Toast.makeText(requireContext(), R.string.ConversationActivity_message_is_empty_exclamation,
- Toast.LENGTH_SHORT).show();
- Log.w(TAG, ex);
- }
- }
-
- private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
- if (ExpiredBuildReminder.isEligible()) {
- showExpiredDialog();
- return;
- }
-
- if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && result.getBodyRanges() != null && result.getBodyRanges().getRangesCount() > 0) {
- Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(result));
- return;
- }
-
- long thread = this.threadId;
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
- List mentions = new ArrayList<>(result.getMentions());
- OutgoingMessage message = new OutgoingMessage(recipient.get(),
- result.getBody(),
- Collections.emptyList(),
- System.currentTimeMillis(),
- expiresIn,
- result.isViewOnce(),
- distributionType,
- result.getStoryType(),
- null,
- false,
- quote,
- Collections.emptyList(),
- Collections.emptyList(),
- mentions,
- Collections.emptySet(),
- Collections.emptySet(),
- null,
- true,
- result.getBodyRanges(),
- -1,
- 0);
-
- final Context context = requireContext().getApplicationContext();
-
- ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
-
- inputPanel.clearQuote();
- attachmentManager.clear(glideRequests, false);
- silentlySetComposeText("");
-
- fragment.stageOutgoingMessage(message);
-
- SimpleTask.run(() -> {
- long resultId = MessageSender.sendPushWithPreUploadedMedia(context, message, result.getPreUploadResults(), thread, null);
-
- int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
- Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
-
- return resultId;
- }, this::sendComplete);
- }
-
- private void sendMediaMessage(@NonNull MessageSendType sendType, final long expiresIn, final boolean viewOnce, final boolean initiating, @Nullable String metricId, long scheduledDate, @Nullable MessageId editMessageId)
- throws InvalidMessageException
- {
- Log.i(TAG, "Sending media message...");
- List linkPreviews = linkPreviewViewModel.onSend();
- sendMediaMessage(recipient.getId(),
- sendType,
- getMessage(),
- attachmentManager.buildSlideDeck(),
- inputPanel.getQuote().orElse(null),
- Collections.emptyList(),
- linkPreviews,
- composeText.getMentions(),
- composeText.getStyling(),
- expiresIn,
- viewOnce,
- initiating,
- true,
- metricId,
- scheduledDate,
- editMessageId);
- }
-
- private ListenableFuture sendMediaMessage(@NonNull RecipientId recipientId,
- @NonNull MessageSendType sendType,
- @NonNull String body,
- SlideDeck slideDeck,
- QuoteModel quote,
- List contacts,
- List previews,
- List mentions,
- @Nullable BodyRangeList styling,
- final long expiresIn,
- final boolean viewOnce,
- final boolean initiating,
- final boolean clearComposeBox,
- final @Nullable String metricId)
- {
- return sendMediaMessage(recipientId, sendType, body, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, -1, null);
- }
-
- private ListenableFuture sendMediaMessage(@NonNull RecipientId recipientId,
- @NonNull MessageSendType sendType,
- @NonNull String body,
- SlideDeck slideDeck,
- QuoteModel quote,
- List contacts,
- List previews,
- List mentions,
- @Nullable BodyRangeList styling,
- final long expiresIn,
- final boolean viewOnce,
- final boolean initiating,
- final boolean clearComposeBox,
- final @Nullable String metricId,
- final long scheduledDate,
- @Nullable MessageId editMessageId)
- {
- if (ExpiredBuildReminder.isEligible()) {
- showExpiredDialog();
- return new SettableFuture<>(null);
- }
-
- if (!viewModel.isDefaultSmsApplication() && sendType.usesSmsTransport() && recipient.get().hasSmsAddress()) {
- showDefaultSmsPrompt();
- return new SettableFuture<>(null);
- }
-
- if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && styling != null && styling.getRangesCount() > 0) {
- final String finalBody = body;
- Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(recipientId, sendType, finalBody, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, scheduledDate, editMessageId));
- return new SettableFuture<>(null);
- }
-
- final boolean sendPush = sendType.usesSignalTransport();
- final long thread = this.threadId;
-
- if (sendPush) {
- MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(requireContext(), body, sendButton.getSelectedSendType().calculateCharacters(body).maxPrimaryMessageSize);
- body = splitMessage.getBody();
-
- if (splitMessage.getTextSlide().isPresent()) {
- slideDeck.addSlide(splitMessage.getTextSlide().get());
- }
- }
-
- OutgoingMessage outgoingMessageCandidate = new OutgoingMessage(Recipient.resolved(recipientId),
- OutgoingMessage.buildMessage(slideDeck, body),
- slideDeck.asAttachments(),
- System.currentTimeMillis(),
- expiresIn,
- viewOnce,
- distributionType,
- StoryType.NONE,
- null,
- false,
- quote,
- contacts,
- previews,
- mentions,
- Collections.emptySet(),
- Collections.emptySet(),
- null,
- false,
- styling,
- scheduledDate,
- editMessageId != null ? editMessageId.getId() : 0);
-
- final SettableFuture future = new SettableFuture<>();
- final Context context = requireContext().getApplicationContext();
-
- final OutgoingMessage outgoingMessage;
-
- if (sendPush) {
- outgoingMessage = outgoingMessageCandidate.makeSecure();
- ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
- } else {
- outgoingMessage = outgoingMessageCandidate.withExpiry(0);
- }
-
- Permissions.with(this)
- .request(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS)
- .ifNecessary(!sendPush)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms))
- .onAllGranted(() -> {
- if (clearComposeBox) {
- inputPanel.clearQuote();
- attachmentManager.clear(glideRequests, false);
- silentlySetComposeText("");
- }
-
- fragment.stageOutgoingMessage(outgoingMessage);
-
- SimpleTask.run(() -> {
- return MessageSender.send(context, outgoingMessage, thread, sendType.usesSmsTransport() ? SendType.MMS : SendType.SIGNAL, metricId, null);
- }, result -> {
- sendComplete(result);
- future.set(null);
- });
- })
- .onAnyDenied(() -> future.set(null))
- .execute();
-
- return future;
- }
-
- private void sendTextMessage(@NonNull MessageSendType sendType,
- final long expiresIn,
- final boolean initiating,
- final @Nullable String metricId,
- long scheduledDate,
- @Nullable MessageId messageToEdit)
- throws InvalidMessageException
- {
- if (ExpiredBuildReminder.isEligible()) {
- showExpiredDialog();
- return;
- }
-
- if (!viewModel.isDefaultSmsApplication() && sendType.usesSmsTransport() && recipient.get().hasSmsAddress()) {
- showDefaultSmsPrompt();
- return;
- }
-
- final long thread = this.threadId;
- final Context context = requireContext().getApplicationContext();
- final String messageBody = getMessage();
- final boolean sendPush = sendType.usesSignalTransport();
-
- final OutgoingMessage message;
-
- if (sendPush) {
- if (messageToEdit != null) {
- message = OutgoingMessage.editText(recipient.get(), messageBody, System.currentTimeMillis(), null, messageToEdit.getId());
- } else if (scheduledDate > 0) {
- message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null)
- .sendAt(scheduledDate);
- } else {
- message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null);
- }
- ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
- } else {
- message = OutgoingMessage.sms(recipient.get(), messageBody);
- }
-
- Permissions.with(this)
- .request(Manifest.permission.SEND_SMS)
- .ifNecessary(!sendPush)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_sms_permission_in_order_to_send_an_sms))
- .onAllGranted(() -> {
- SimpleTask.run(() -> {
- return MessageSender.send(context, message, thread, sendType.usesSmsTransport() ? SendType.SMS : SendType.SIGNAL, metricId, null);
- }, this::sendComplete);
-
- silentlySetComposeText("");
- fragment.stageOutgoingMessage(message);
- })
- .execute();
- }
-
- private void showDefaultSmsPrompt() {
- new MaterialAlertDialogBuilder(requireContext())
- .setMessage(R.string.ConversationActivity_signal_cannot_sent_sms_mms_messages_because_it_is_not_your_default_sms_app)
- .setNegativeButton(R.string.ConversationActivity_no, (dialog, which) -> dialog.dismiss())
- .setPositiveButton(R.string.ConversationActivity_yes, (dialog, which) -> handleMakeDefaultSms())
- .show();
- }
-
- private void showExpiredDialog() {
- Reminder reminder = new ExpiredBuildReminder(requireContext());
-
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
- .setMessage(reminder.getText(requireContext()))
- .setPositiveButton(android.R.string.ok, (d, w) -> d.dismiss());
-
- List actions = reminder.getActions();
- if (actions.size() == 1) {
- Reminder.Action action = actions.get(0);
-
- builder.setNeutralButton(action.getTitle(requireContext()), (d, i) -> {
- if (action.getActionId() == R.id.reminder_action_update_now) {
- PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
- }
- });
- }
-
- builder.show();
- }
-
- private void updateToggleButtonState() {
- if (inputPanel.isRecordingInLockedMode()) {
- buttonToggle.display(sendButton);
- quickAttachmentToggle.show();
- inlineAttachmentToggle.hide();
- return;
- }
-
- if (inputPanel.inEditMessageMode()) {
- buttonToggle.display(sendEditButton);
- quickAttachmentToggle.hide();
- inlineAttachmentToggle.hide();
- return;
- }
-
- if (draftViewModel.getVoiceNoteDraft() != null) {
- buttonToggle.display(sendButton);
- quickAttachmentToggle.hide();
- inlineAttachmentToggle.hide();
- return;
- }
-
- if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
- buttonToggle.display(attachButton);
- quickAttachmentToggle.show();
- inlineAttachmentToggle.hide();
- } else {
- buttonToggle.display(sendButton);
- quickAttachmentToggle.hide();
-
- if (!attachmentManager.isAttachmentPresent() && !linkPreviewViewModel.hasLinkPreviewUi()) {
- inlineAttachmentToggle.show();
- } else {
- inlineAttachmentToggle.hide();
- }
- }
- }
-
- private void onViewModelEvent(@NonNull ConversationViewModel.Event event) {
- if (event == ConversationViewModel.Event.SHOW_RECAPTCHA) {
- RecaptchaProofBottomSheetFragment.show(getChildFragmentManager());
- } else {
- throw new AssertionError("Unexpected event!");
- }
- }
-
- private void updateLinkPreviewState() {
- if (SignalStore.settings().isLinkPreviewsEnabled() && viewModel.isPushAvailable() && !sendButton.getSelectedSendType().usesSmsTransport() && !attachmentManager.isAttachmentPresent() && getContext() != null) {
- linkPreviewViewModel.onEnabled();
- linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
- } else {
- linkPreviewViewModel.onUserCancel();
- }
- }
-
- private void updateScheduledMessagesBar(int count) {
- if (count <= 0) {
- scheduledMessagesBarStub.setVisibility(View.GONE);
- reshowScheduleMessagesBar = false;
- } else {
- View scheduledMessagesBar = scheduledMessagesBarStub.get();
-
- scheduledMessagesBar.findViewById(R.id.scheduled_messages_show_all).setOnClickListener(v -> {
- ScheduledMessagesBottomSheet.show(getChildFragmentManager(), threadId, recipient.getId());
- });
-
- scheduledMessagesBar.setVisibility(View.VISIBLE);
- reshowScheduleMessagesBar = true;
- TextView scheduledText = scheduledMessagesBar.findViewById(R.id.scheduled_messages_text);
- scheduledText.setText(getResources().getQuantityString(R.plurals.conversation_scheduled_messages_bar__number_of_messages, count, count));
- }
- }
-
- @Override
- public void onRecorderPermissionRequired() {
- Permissions.with(this)
- .request(Manifest.permission.RECORD_AUDIO)
- .ifNecessary()
- .withRationaleDialog(getString(R.string.ConversationActivity_to_send_audio_messages_allow_signal_access_to_your_microphone), R.drawable.ic_mic_solid_24)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_requires_the_microphone_permission_in_order_to_send_audio_messages))
- .execute();
- }
-
- @Override
- public void onRecorderStarted() {
- beginRecording();
- }
-
- private Unit onBluetoothConnectionAttempt(Boolean success) {
- beginRecording();
- return Unit.INSTANCE;
- }
-
- private Unit beginRecording() {
- Vibrator vibrator = ServiceUtil.getVibrator(requireContext());
- vibrator.vibrate(20);
-
- requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
-
- voiceNoteMediaController.pausePlayback();
- recordingSession = new RecordingSession(audioRecorder.startRecording());
- disposables.add(recordingSession);
- return Unit.INSTANCE;
- }
-
- private Unit onBluetoothPermissionDenied() {
- new MaterialAlertDialogBuilder(requireContext())
- .setTitle(R.string.ConversationParentFragment__bluetooth_permission_denied)
- .setMessage(R.string.ConversationParentFragment__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
- .setPositiveButton(R.string.ConversationParentFragment__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(requireContext())))
- .setNegativeButton(R.string.ConversationParentFragment__not_now, null)
- .show();
-
- return Unit.INSTANCE;
- }
-
- @Override
- public void onRecorderLocked() {
- voiceRecorderWakeLock.acquire();
- updateToggleButtonState();
- requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- }
-
- @Override
- public void onRecorderFinished() {
- voiceRecorderWakeLock.release();
- updateToggleButtonState();
-
- Activity activity = getActivity();
- if (activity != null) {
- Vibrator vibrator = ServiceUtil.getVibrator(activity);
- vibrator.vibrate(20);
-
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- }
-
- if (recordingSession != null) {
- recordingSession.completeRecording();
- }
- }
-
- @Override
- public void onRecorderCanceled(boolean byUser) {
- voiceRecorderWakeLock.release();
- updateToggleButtonState();
-
- Activity activity = getActivity();
- if (activity != null) {
- Vibrator vibrator = ServiceUtil.getVibrator(activity);
- vibrator.vibrate(50);
-
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
- }
-
- if (recordingSession != null) {
- if (byUser) {
- recordingSession.discardRecording();
- } else {
- recordingSession.saveDraft();
- }
- }
- }
-
- @Override
- public void onEmojiToggle() {
- if (!emojiDrawerStub.resolved()) {
- initializeMediaKeyboardProviders();
- }
-
- inputPanel.setMediaKeyboard(emojiDrawerStub.get());
- emojiDrawerStub.get().setFragmentManager(getChildFragmentManager());
-
- if (container.getCurrentInput() == emojiDrawerStub.get()) {
- container.showSoftkey(composeText);
- } else {
- container.show(composeText, emojiDrawerStub.get());
- }
- }
-
- @Override
- public void onLinkPreviewCanceled() {
- linkPreviewViewModel.onUserCancel();
- }
-
- @Override
- public void onStickerSuggestionSelected(@NonNull StickerRecord sticker) {
- sendSticker(sticker, true);
- }
-
- @Override
- public void onQuoteChanged(long id, @NonNull RecipientId author) {
- draftViewModel.setQuoteDraft(id, author);
- }
-
- @Override
- public void onQuoteCleared() {
- draftViewModel.clearQuoteDraft();
- }
-
- @Override
- public void onMediaSelected(@NonNull Uri uri, String contentType) {
- if (MediaUtil.isGif(contentType) || MediaUtil.isImageType(contentType)) {
- SimpleTask.run(getLifecycle(),
- () -> getKeyboardImageDetails(uri),
- details -> sendKeyboardImage(uri, contentType, details));
- } else if (MediaUtil.isVideoType(contentType)) {
- setMedia(uri, MediaType.VIDEO);
- } else if (MediaUtil.isAudioType(contentType)) {
- setMedia(uri, MediaType.AUDIO);
- }
- }
-
- @Override
- public void onCursorPositionChanged(int start, int end) {
- linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), start, end);
- }
-
- @Override
- public void onStickerSelected(@NonNull StickerRecord stickerRecord) {
- sendSticker(stickerRecord, false);
- }
-
- @Override
- public void onStickerManagementClicked() {
- startActivity(StickerManagementActivity.getIntent(requireContext()));
- container.hideAttachedInput(true);
- }
-
- private void sendVoiceNote(@NonNull Uri uri, long size, long scheduledDate) {
- boolean initiating = threadId == -1;
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- AudioSlide audioSlide = new AudioSlide(requireContext(), uri, size, MediaUtil.AUDIO_AAC, true);
- SlideDeck slideDeck = new SlideDeck();
-
- slideDeck.addSlide(audioSlide);
-
- sendMediaMessage(recipient.getId(),
- sendButton.getSelectedSendType(),
- "",
- slideDeck,
- inputPanel.getQuote().orElse(null),
- Collections.emptyList(),
- Collections.emptyList(),
- composeText.getMentions(),
- composeText.getStyling(),
- expiresIn,
- false,
- initiating,
- true,
- null,
- scheduledDate,
- null);
- }
-
- private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
- sendSticker(new StickerLocator(stickerRecord.getPackId(), stickerRecord.getPackKey(), stickerRecord.getStickerId(), stickerRecord.getEmoji()), stickerRecord.getContentType(), stickerRecord.getUri(), stickerRecord.getSize(), clearCompose);
-
- SignalExecutors.BOUNDED.execute(() ->
- SignalDatabase.stickers()
- .updateStickerLastUsedTime(stickerRecord.getRowId(), System.currentTimeMillis())
- );
- }
-
- private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
- if (sendButton.getSelectedSendType().usesSmsTransport()) {
- Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty());
- Intent intent = MediaSelectionActivity.editor(requireContext(), sendButton.getSelectedSendType(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed());
- startActivityForResult(intent, MEDIA_SENDER);
- return;
- }
-
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- boolean initiating = threadId == -1;
- MessageSendType sendType = sendButton.getSelectedSendType();
- SlideDeck slideDeck = new SlideDeck();
- Slide stickerSlide = new StickerSlide(requireContext(), uri, size, stickerLocator, contentType);
-
- slideDeck.addSlide(stickerSlide);
-
- sendMediaMessage(recipient.getId(), sendType, "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, clearCompose, null);
- }
-
- private void silentlySetComposeText(String text) {
- typingTextWatcher.setTypingStatusEnabled(false);
- composeText.setText(text);
- typingTextWatcher.setTypingStatusEnabled(true);
- }
-
- @Override
- public void onReactionsDialogDismissed() {
- fragment.clearFocusedItem();
- }
-
- @Override
- public void onShown() {
- if (inputPanel != null) {
- inputPanel.getMediaKeyboardListener().onShown();
- }
- }
-
- @Override
- public void onHidden() {
- if (inputPanel != null) {
- inputPanel.getMediaKeyboardListener().onHidden();
- }
- }
-
- @Override
- public void onKeyboardChanged(@NonNull KeyboardPage page) {
- if (inputPanel != null) {
- inputPanel.getMediaKeyboardListener().onKeyboardChanged(page);
- }
- }
-
- @Override
- public void onEmojiSelected(String emoji) {
- if (inputPanel != null) {
- inputPanel.onEmojiSelected(emoji);
- if (recentEmojis == null) {
- recentEmojis = new RecentEmojiPageModel(ApplicationDependencies.getApplication(), TextSecurePreferences.RECENT_STORAGE_KEY);
- }
- recentEmojis.onCodePointSelected(emoji);
- }
- }
-
- @Override
- public void onKeyEvent(KeyEvent keyEvent) {
- if (keyEvent != null) {
- inputPanel.onKeyEvent(keyEvent);
- }
- }
-
- @Override
- public void openGifSearch() {
- AttachmentManager.selectGif(this, ConversationParentFragment.PICK_GIF, recipient.getId(), sendButton.getSelectedSendType(), isMms(), composeText.getTextTrimmed());
- }
-
- @Override
- public void onGifSelectSuccess(@NonNull Uri blobUri, int width, int height) {
- setMedia(blobUri,
- Objects.requireNonNull(MediaType.from(BlobProvider.getMimeType(blobUri))),
- width,
- height,
- false,
- true);
- }
-
- @Override
- public boolean isMms() {
- return !viewModel.isPushAvailable();
- }
-
- @Override
- public void openEmojiSearch() {
- if (emojiDrawerStub.resolved()) {
- emojiDrawerStub.get().onOpenEmojiSearch();
- }
- }
-
- @Override public void closeEmojiSearch() {
- if (emojiDrawerStub.resolved()) {
- emojiDrawerStub.get().onCloseEmojiSearch();
- }
- }
-
- @Override
- public void onVoiceNoteDraftPlay(@NonNull Uri audioUri, double progress) {
- voiceNoteMediaController.startSinglePlaybackForDraft(audioUri, threadId, progress);
- }
-
- @Override
- public void onVoiceNoteDraftPause(@NonNull Uri audioUri) {
- voiceNoteMediaController.pausePlayback(audioUri);
- }
-
- @Override
- public void onVoiceNoteDraftSeekTo(@NonNull Uri audioUri, double progress) {
- voiceNoteMediaController.seekToPosition(audioUri, progress);
- }
-
- @Override
- public void onVoiceNoteDraftDelete(@NonNull Uri audioUri) {
- voiceNoteMediaController.stopPlaybackAndReset(audioUri);
- draftViewModel.deleteVoiceNoteDraft();
- }
-
- @Override
- public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
- return voiceNoteMediaController;
- }
-
- @Override public void openStickerSearch() {
- StickerSearchDialogFragment.show(getChildFragmentManager());
- }
-
- @Override
- public void bindScrollHelper(@NonNull RecyclerView recyclerView) {
- material3OnScrollHelper.attach(recyclerView);
- }
-
- @Override
- public void onMessageDetailsFragmentDismissed() {
- material3OnScrollHelper.setColorImmediate();
- }
-
- @Override
- public void sendAnywayAfterSafetyNumberChangedInBottomSheet(@NonNull List destinations) {
- Log.d(TAG, "onSendAnywayAfterSafetyNumberChange");
- initializeIdentityRecords().addListener(new AssertedSuccessListener() {
- @Override
- public void onSuccess(Boolean result) {
- sendMessage(null);
- }
- });
- }
-
- @Override
- public void onScheduleSend(long scheduledTime) {
- sendMessage(null, scheduledTime);
- }
-
- @Override
- public @NonNull ConversationAdapter.ItemClickListener getConversationAdapterListener() {
- return fragment.getConversationAdapterListener();
- }
-
- @Override
- public void jumpToMessage(@NonNull MessageRecord messageRecord) {
- fragment.jumpToMessage(messageRecord);
- }
-
- @Override
- public void onSchedulePermissionsGranted(@Nullable String metricId, long scheduledDate) {
- sendMessage(metricId, scheduledDate);
- }
-
- @Override
- public void handleGoHome() {
- requireActivity().finish();
- }
-
- @Override
- public @NonNull ConversationOptionsMenu.Snapshot getSnapshot() {
- ConversationGroupViewModel.GroupActiveState groupActiveState = groupViewModel.getGroupActiveState().getValue();
-
- return new ConversationOptionsMenu.Snapshot(
- recipient != null ? recipient.get() : null,
- viewModel.isPushAvailable(),
- viewModel.canShowAsBubble(),
- groupActiveState != null && groupActiveState.isActiveGroup(),
- groupActiveState != null && groupActiveState.isActiveV2Group(),
- groupActiveState != null && !groupActiveState.isActiveGroup(),
- groupCallViewModel != null && groupCallViewModel.hasActiveGroupCall().getValue() == Boolean.TRUE,
- distributionType,
- threadId,
- isInMessageRequest(),
- isInBubble()
- );
- }
-
- @Override
- public boolean isTextHighlighted() {
- return composeText.isTextHighlighted();
- }
-
- @Override
- public void showExpiring(@NonNull Recipient recipient) {
- titleView.showExpiring(recipient);
- }
-
- @Override
- public void clearExpiring() {
- titleView.clearExpiring();
- }
-
- // Listeners
-
- private class RecordingSession implements SingleObserver, Disposable {
-
- private boolean saveDraft = true;
- private boolean shouldSend = false;
- private Disposable disposable = Disposable.empty();
-
- RecordingSession(Single observable) {
- observable.observeOn(AndroidSchedulers.mainThread()).subscribe(this);
- }
-
- @Override
- public void onSubscribe(@io.reactivex.rxjava3.annotations.NonNull Disposable d) {
- this.disposable = d;
- }
-
- @Override
- public void onSuccess(@NonNull VoiceNoteDraft draft) {
- if (shouldSend) {
- sendVoiceNote(draft.getUri(), draft.getSize(), -1);
- } else {
- if (!saveDraft) {
- draftViewModel.cancelEphemeralVoiceNoteDraft(draft.asDraft());
- } else {
- draftViewModel.saveEphemeralVoiceNoteDraft(draft.asDraft());
- }
- }
-
- recordingSession.dispose();
- recordingSession = null;
- }
-
- @Override
- public void onError(Throwable t) {
- Toast.makeText(requireContext(), R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show();
- Log.e(TAG, "Error in RecordingSession.", t);
- recordingSession.discardRecording();
- recordingSession.dispose();
- recordingSession = null;
- }
-
- public void saveDraft() {
- this.saveDraft = true;
- this.shouldSend = false;
- audioRecorder.stopRecording();
- }
-
- public void discardRecording() {
- this.saveDraft = false;
- this.shouldSend = false;
- audioRecorder.stopRecording();
- }
-
- public void completeRecording() {
- this.shouldSend = true;
- audioRecorder.stopRecording();
- }
-
- @Override
- public void dispose() {
- disposable.dispose();
- }
-
- @Override
- public boolean isDisposed() {
- return disposable.isDisposed();
- }
- }
-
- private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
- @Override
- public void onClick(View v) {
- String metricId = recipient.get().isGroup() ? SignalLocalMetrics.GroupMessageSend.start()
- : SignalLocalMetrics.IndividualMessageSend.start();
- sendMessage(metricId);
- }
-
- @Override
- public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
- if (actionId == EditorInfo.IME_ACTION_SEND) {
- if (inputPanel.isInEditMode()) {
- sendEditButton.performClick();
- } else {
- sendButton.performClick();
- }
- return true;
- }
- return false;
- }
- }
-
- private class AttachButtonListener implements OnClickListener {
- @Override
- public void onClick(View v) {
- handleAddAttachment();
- }
- }
-
- private class AttachButtonLongClickListener implements View.OnLongClickListener {
- @Override
- public boolean onLongClick(View v) {
- return sendButton.showSendTypeMenu();
- }
- }
-
- private class ComposeKeyPressedListener implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener {
-
- int beforeLength;
-
- @Override
- public boolean onKey(View v, int keyCode, KeyEvent event) {
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
- if (keyCode == KeyEvent.KEYCODE_ENTER) {
- if (SignalStore.settings().isEnterKeySends() || event.isCtrlPressed()) {
- sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
- sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
- return true;
- }
- }
- }
- return false;
- }
-
- @Override
- public void onClick(View v) {
- container.showSoftkey(composeText);
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count,int after) {
- beforeLength = composeText.getTextTrimmed().length();
- }
-
- @Override
- public void afterTextChanged(Editable s) {
- calculateCharactersRemaining();
-
- if (composeText.getTextTrimmed().length() == 0 || beforeLength == 0) {
- composeText.postDelayed(ConversationParentFragment.this::updateToggleButtonState, 50);
- }
-
- if (!inputPanel.inEditMessageMode()) {
- stickerViewModel.onInputTextUpdated(s.toString());
- } else {
- stickerViewModel.onInputTextUpdated("");
- }
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before,int count) {}
-
- @Override
- public void onFocusChange(View v, boolean hasFocus) {
- if (hasFocus && container.getCurrentInput() == emojiDrawerStub.get()) {
- container.showSoftkey(composeText);
- }
- }
- }
-
- private class ComposeTextWatcher extends SimpleTextWatcher implements ComposeText.StylingChangedListener {
-
- private boolean typingStatusEnabled = true;
-
- private String previousText = "";
-
- @Override
- public void onTextChanged(@NonNull CharSequence text) {
- handleSaveDraftOnTextChange(composeText.getTextTrimmed());
- handleTypingIndicatorOnTextChange(text.toString());
- }
-
- private void handleSaveDraftOnTextChange(@NonNull CharSequence text) {
- textDraftSaveDebouncer.publish(() -> {
- if (inputPanel.inEditMessageMode()) {
- draftViewModel.setMessageEditDraft(inputPanel.getEditMessageId(), StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text));
- } else {
- draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text));
- }
- });
- }
-
- private void handleTypingIndicatorOnTextChange(@NonNull String text) {
- if (typingStatusEnabled && threadId > 0 && viewModel.isPushAvailable() && !isSmsForced() && !recipient.get().isBlocked() && !recipient.get().isSelf()) {
- TypingStatusSender typingStatusSender = ApplicationDependencies.getTypingStatusSender();
-
- if (text.length() == 0) {
- typingStatusSender.onTypingStoppedWithNotify(threadId);
- } else if (text.length() < previousText.length() && previousText.contains(text)) {
- typingStatusSender.onTypingStopped(threadId);
- } else {
- typingStatusSender.onTypingStarted(threadId);
- }
-
- previousText = text;
- }
- }
-
- public void setTypingStatusEnabled(boolean enabled) {
- this.typingStatusEnabled = enabled;
- }
-
- @Override
- public void onStylingChanged() {
- handleSaveDraftOnTextChange(composeText.getTextTrimmed());
- }
- }
-
- @Override
- public void onMessageRequest(@NonNull MessageRequestViewModel viewModel) {
- messageRequestBottomView.setAcceptOnClickListener(v -> viewModel.onAccept());
- messageRequestBottomView.setDeleteOnClickListener(v -> onMessageRequestDeleteClicked(viewModel));
- messageRequestBottomView.setBlockOnClickListener(v -> onMessageRequestBlockClicked(viewModel));
- messageRequestBottomView.setUnblockOnClickListener(v -> onMessageRequestUnblockClicked(viewModel));
- messageRequestBottomView.setGroupV1MigrationContinueListener(v -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(getChildFragmentManager(), recipient.getId()));
-
- viewModel.getRequestReviewDisplayState().observe(getViewLifecycleOwner(), this::presentRequestReviewBanner);
- viewModel.getMessageData().observe(getViewLifecycleOwner(), this::presentMessageRequestState);
- viewModel.getFailures().observe(getViewLifecycleOwner(), this::showGroupChangeErrorToast);
- viewModel.getMessageRequestStatus().observe(getViewLifecycleOwner(), status -> {
- switch (status) {
- case IDLE:
- hideMessageRequestBusy();
- break;
- case ACCEPTING:
- case BLOCKING:
- case DELETING:
- showMessageRequestBusy();
- break;
- case ACCEPTED:
- hideMessageRequestBusy();
- break;
- case BLOCKED_AND_REPORTED:
- hideMessageRequestBusy();
- Toast.makeText(requireContext(), R.string.ConversationActivity__reported_as_spam_and_blocked, Toast.LENGTH_SHORT).show();
- break;
- case DELETED:
- case BLOCKED:
- hideMessageRequestBusy();
- requireActivity().finish();
- }
- });
- }
-
- private void presentRequestReviewBanner(@NonNull MessageRequestViewModel.RequestReviewDisplayState state) {
- switch (state) {
- case SHOWN:
- reviewBanner.get().setVisibility(View.VISIBLE);
-
- CharSequence message = new SpannableStringBuilder().append(SpanUtil.bold(getString(R.string.ConversationFragment__review_requests_carefully)))
- .append(" ")
- .append(getString(R.string.ConversationFragment__signal_found_another_contact_with_the_same_name));
-
- reviewBanner.get().setBannerMessage(message);
-
- Drawable drawable = ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_info_24).mutate();
- DrawableCompat.setTint(drawable, ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary));
-
- reviewBanner.get().setBannerIcon(drawable);
- reviewBanner.get().setOnClickListener(unused -> handleReviewRequest(recipient.getId()));
- break;
- case HIDDEN:
- reviewBanner.get().setVisibility(View.GONE);
- break;
- default:
- break;
- }
- }
-
- private void presentGroupReviewBanner(@NonNull ConversationGroupViewModel.ReviewState groupReviewState) {
- if (groupReviewState.getCount() > 0) {
- reviewBanner.get().setVisibility(View.VISIBLE);
- reviewBanner.get().setBannerMessage(getString(R.string.ConversationFragment__d_group_members_have_the_same_name, groupReviewState.getCount()));
- reviewBanner.get().setBannerRecipient(groupReviewState.getRecipient());
- reviewBanner.get().setOnClickListener(unused -> handleReviewGroupMembers(groupReviewState.getGroupId()));
- } else if (reviewBanner.resolved()) {
- reviewBanner.get().setVisibility(View.GONE);
- }
- }
-
- private void showMessageRequestBusy() {
- messageRequestBottomView.showBusy();
- }
-
- private void hideMessageRequestBusy() {
- messageRequestBottomView.hideBusy();
- }
-
- private void handleReviewGroupMembers(@Nullable GroupId.V2 groupId) {
- if (groupId == null) {
- return;
- }
-
- ReviewCardDialogFragment.createForReviewMembers(groupId)
- .show(getChildFragmentManager(), null);
- }
-
- private void handleReviewRequest(@NonNull RecipientId recipientId) {
- if (recipientId == Recipient.UNKNOWN.getId()) {
- return;
- }
-
- ReviewCardDialogFragment.createForReviewRequest(recipientId)
- .show(getChildFragmentManager(), null);
- }
-
- private void showGroupChangeErrorToast(@NonNull GroupChangeFailureReason e) {
- Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void handleReaction(@NonNull ConversationMessage conversationMessage,
- @NonNull ConversationReactionOverlay.OnActionSelectedListener onActionSelectedListener,
- @NonNull SelectedConversationModel selectedConversationModel,
- @NonNull ConversationReactionOverlay.OnHideListener onHideListener)
- {
- reactionDelegate.setOnActionSelectedListener(onActionSelectedListener);
- reactionDelegate.setOnHideListener(onHideListener);
- reactionDelegate.show(requireActivity(), recipient.get(), conversationMessage, groupViewModel.isNonAdminInAnnouncementGroup(), selectedConversationModel);
- composeText.clearFocus();
- if (attachmentKeyboardStub.resolved()) {
- attachmentKeyboardStub.get().hide(true);
- }
- }
-
- @Override
- public void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord) {
- if (messageRecord.isIdentityMismatchFailure()) {
- SafetyNumberBottomSheet
- .forMessageRecord(requireContext(), messageRecord)
- .show(getChildFragmentManager());
- } else if (messageRecord.hasFailedWithNetworkFailures()) {
- ConversationDialogs.INSTANCE.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord);
- } else {
- MessageDetailsFragment.create(messageRecord, recipient.getId()).show(getChildFragmentManager(), null);
- }
- }
-
- @Override
- public void onFirstRender() {
- if (getActivity() != null) {
- requireActivity().supportStartPostponedEnterTransition();
- }
- voiceNoteMediaController.finishPostpone();
- }
-
- @Override
- public void onVoiceNotePause(@NonNull Uri uri) {
- voiceNoteMediaController.pausePlayback(uri);
- }
-
- @Override
- public void onVoiceNotePlay(@NonNull Uri uri, long messageId, double progress) {
- voiceNoteMediaController.startConsecutivePlayback(uri, messageId, progress);
- }
-
- @Override
- public void onVoiceNoteResume(@NonNull Uri uri, long messageId) {
- voiceNoteMediaController.resumePlayback(uri, messageId);
- }
-
- @Override
- public void onVoiceNoteSeekTo(@NonNull Uri uri, double progress) {
- voiceNoteMediaController.seekToPosition(uri, progress);
- }
-
- @Override
- public void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed) {
- voiceNoteMediaController.setPlaybackSpeed(uri, speed);
- }
-
- @Override
- public void onRegisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver) {
- voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), onPlaybackStartObserver);
- }
-
- @Override
- public void onUnregisterVoiceNoteCallbacks(@NonNull Observer onPlaybackStartObserver) {
- voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(onPlaybackStartObserver);
- }
-
- @Override
- public void onInviteToSignal() {
- handleInviteLink();
- }
-
- @Override
- public void onDeleteMessage(long id) {
- MessageId messageId = inputPanel.getEditMessageId();
- if (messageId != null && messageId.getId() == id) {
- inputPanel.exitEditMessageMode();
- }
- }
-
- @Override
- public void onRemoteDeleteMessage(long targetId) {
- MessageId messageId = inputPanel.getEditMessageId();
- if (messageId != null && messageId.getId() == targetId) {
- inputPanel.exitEditMessageMode();
- }
- }
-
- @Override
- public void onCursorChanged() {
- if (!reactionDelegate.isShowing()) {
- return;
- }
-
- SimpleTask.run(() -> {
- //noinspection CodeBlock2Expr
- return SignalDatabase.messages().messageExists(reactionDelegate.getMessageRecord());
- }, messageExists -> {
- if (!messageExists) {
- reactionDelegate.hide();
- }
- });
- }
-
- @Override
- public int getSendButtonTint() {
- return getSendButtonColor(sendButton.getSelectedSendType());
- }
-
- @Override
- public boolean isKeyboardOpen() {
- return container.isKeyboardOpen();
- }
-
- @Override
- public boolean isAttachmentKeyboardOpen() {
- return attachmentKeyboardStub.resolved() && attachmentKeyboardStub.get().isShowing();
- }
-
- @Override
- public void openAttachmentKeyboard() {
- attachmentKeyboardStub.get().show(container.getKeyboardHeight(), true);
- }
-
- @Override
- public void setThreadId(long threadId) {
- this.threadId = threadId;
- draftViewModel.setThreadId(threadId);
- }
-
- @Override
- public void handleReplyMessage(ConversationMessage conversationMessage) {
- if (isSearchRequested) {
- searchViewItem.collapseActionView();
- }
- if (inputPanel.inEditMessageMode()) {
- inputPanel.exitEditMessageMode();
- }
-
- MessageRecord messageRecord = conversationMessage.getMessageRecord();
-
- Recipient author = messageRecord.getFromRecipient();
-
- if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getSharedContacts().isEmpty()) {
- Contact contact = ((MmsMessageRecord) messageRecord).getSharedContacts().get(0);
- String displayName = ContactUtil.getDisplayName(contact);
- String body = getString(R.string.ConversationActivity_quoted_contact_message, EmojiStrings.BUST_IN_SILHOUETTE, displayName);
- SlideDeck slideDeck = new SlideDeck();
-
- if (contact.getAvatarAttachment() != null) {
- slideDeck.addSlide(MediaUtil.getSlideForAttachment(requireContext(), contact.getAvatarAttachment()));
- }
-
- inputPanel.setQuote(GlideApp.with(this),
- messageRecord.getDateSent(),
- author,
- body,
- slideDeck,
- MessageRecordUtil.getRecordQuoteType(messageRecord));
-
- } else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
- LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
- SlideDeck slideDeck = new SlideDeck();
-
- if (linkPreview.getThumbnail().isPresent()) {
- slideDeck.addSlide(MediaUtil.getSlideForAttachment(requireContext(), linkPreview.getThumbnail().get()));
- }
-
- inputPanel.setQuote(GlideApp.with(this),
- messageRecord.getDateSent(),
- author,
- conversationMessage.getDisplayBody(requireContext()),
- slideDeck,
- MessageRecordUtil.getRecordQuoteType(messageRecord));
- } else {
- SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
-
- if (messageRecord.isMms() && messageRecord.isViewOnce()) {
- Attachment attachment = new TombstoneAttachment(MediaUtil.VIEW_ONCE, true);
- slideDeck = new SlideDeck();
- slideDeck.addSlide(MediaUtil.getSlideForAttachment(requireContext(), attachment));
- }
-
- inputPanel.setQuote(GlideApp.with(this),
- messageRecord.getDateSent(),
- author,
- conversationMessage.getDisplayBody(requireContext()),
- slideDeck,
- MessageRecordUtil.getRecordQuoteType(messageRecord));
- }
-
- inputPanel.clickOnComposeInput();
- }
-
- @Override
- public void handleEditMessage(@NonNull ConversationMessage conversationMessage) {
- if (!FeatureFlags.editMessageSending()) {
- return;
- }
- if (isSearchRequested) {
- searchViewItem.collapseActionView();
- }
- disposables.add(viewModel.resolveMessageToEdit(conversationMessage).subscribe(updatedMessage -> {
- inputPanel.enterEditMessageMode(glideRequests, updatedMessage, false);
- }));
- }
-
- private void handleSendEditMessage() {
- if (!FeatureFlags.editMessageSending()) {
- Log.w(TAG, "Edit message sending disabled, forcing exit of edit mode");
- inputPanel.exitEditMessageMode();
- return;
- }
-
- if (!inputPanel.inEditMessageMode()) {
- Log.w(TAG, "Not in edit message mode, unknown state, forcing re-exit");
- inputPanel.exitEditMessageMode();
- return;
- }
-
- if (SignalStore.uiHints().hasNotSeenEditMessageBetaAlert()) {
- Dialogs.showEditMessageBetaDialog(requireContext(), this::handleSendEditMessage);
- return;
- }
-
- MessageRecord editMessage = inputPanel.getEditMessage();
- if (editMessage == null) {
- Log.w(TAG, "No edit message found, forcing exit");
- inputPanel.exitEditMessageMode();
- return;
- }
-
- if (!MessageConstraintsUtil.isValidEditMessageSend(editMessage, System.currentTimeMillis())) {
- Log.i(TAG, "Edit message no longer valid");
- final int editDurationHours = MessageConstraintsUtil.getEditMessageThresholdHours();
- Dialogs.showAlertDialog(requireContext(), null, getResources().getQuantityString(R.plurals.ConversationActivity_edit_message_too_old, editDurationHours, editDurationHours));
- return;
- }
-
- String metricId = recipient.get().isGroup() ? SignalLocalMetrics.GroupMessageSend.start()
- : SignalLocalMetrics.IndividualMessageSend.start();
-
- sendMessage(metricId);
- }
-
- @Override
- public void onEnterEditMode() {
- updateToggleButtonState();
- previousPages = keyboardPagerViewModel.pages().getValue();
- keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI);
- onKeyboardChanged(KeyboardPage.EMOJI);
- stickerViewModel.onInputTextUpdated("");
- }
-
- @Override
- public void onExitEditMode() {
- updateToggleButtonState();
- draftViewModel.deleteMessageEditDraft();
- if (previousPages != null) {
- keyboardPagerViewModel.setPages(previousPages);
- previousPages = null;
- }
- }
-
- @Override
- public void onQuickCameraToggleClicked() {
- Permissions.with(ConversationParentFragment.this)
- .request(Manifest.permission.CAMERA)
- .ifNecessary()
- .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
- .onAllGranted(() -> {
- composeText.clearFocus();
- startActivityForResult(MediaSelectionActivity.camera(requireActivity(), sendButton.getSelectedSendType(), recipient.getId(), inputPanel.getQuote().isPresent()), MEDIA_SENDER);
- requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
- })
- .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
- .execute();
- }
-
- @Override
- public void onMessageActionToolbarOpened() {
- searchViewItem.collapseActionView();
- toolbar.setVisibility(View.GONE);
- if (scheduledMessagesBarStub.getVisibility() == View.VISIBLE) {
- reshowScheduleMessagesBar = true;
- scheduledMessagesBarStub.setVisibility(View.GONE);
- }
- }
-
- @Override
- public void onMessageActionToolbarClosed() {
- toolbar.setVisibility(View.VISIBLE);
- if (reshowScheduleMessagesBar) {
- scheduledMessagesBarStub.setVisibility(View.VISIBLE);
- reshowScheduleMessagesBar = false;
- }
- }
-
- @Override
- public void onBottomActionBarVisibilityChanged(int visibility) {
- inputPanel.setHideForSelection(visibility == View.VISIBLE);
- }
-
- @Override
- public void onForwardClicked() {
- inputPanel.clearQuote();
- }
-
- @Override
- public void onAttachmentChanged() {
- handleSecurityChange(viewModel.getConversationStateSnapshot().getSecurityInfo());
- updateToggleButtonState();
- updateLinkPreviewState();
- }
-
- @Override
- public void onLocationRemoved() {
- draftViewModel.clearLocationDraft();
- }
-
- private void onMessageRequestDeleteClicked(@NonNull MessageRequestViewModel requestModel) {
- Recipient recipient = requestModel.getRecipient().getValue();
- if (recipient == null) {
- Log.w(TAG, "[onMessageRequestDeleteClicked] No recipient!");
- return;
- }
-
- ConversationDialogs.displayDeleteDialog(requireContext(), recipient, () -> {
- requestModel.onDelete();
- return Unit.INSTANCE;
- });
- }
-
- private void onMessageRequestBlockClicked(@NonNull MessageRequestViewModel requestModel) {
- Recipient recipient = requestModel.getRecipient().getValue();
- if (recipient == null) {
- Log.w(TAG, "[onMessageRequestBlockClicked] No recipient!");
- return;
- }
-
- BlockUnblockDialog.showBlockAndReportSpamFor(requireContext(), getLifecycle(), recipient, requestModel::onBlock, requestModel::onBlockAndReportSpam);
- }
-
- private void onMessageRequestUnblockClicked(@NonNull MessageRequestViewModel requestModel) {
- Recipient recipient = requestModel.getRecipient().getValue();
- if (recipient == null) {
- Log.w(TAG, "[onMessageRequestUnblockClicked] No recipient!");
- return;
- }
-
- BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, requestModel::onUnblock);
- }
-
- @WorkerThread
- private @Nullable KeyboardImageDetails getKeyboardImageDetails(@NonNull Uri uri) {
- try {
- Bitmap bitmap = glideRequests.asBitmap()
- .load(new DecryptableStreamUriLoader.DecryptableUri(uri))
- .skipMemoryCache(true)
- .diskCacheStrategy(DiskCacheStrategy.NONE)
- .submit()
- .get(1000, TimeUnit.MILLISECONDS);
- int topLeft = bitmap.getPixel(0, 0);
- return new KeyboardImageDetails(bitmap.getWidth(), bitmap.getHeight(), Color.alpha(topLeft) < 255);
- } catch (InterruptedException | ExecutionException | TimeoutException e) {
- return null;
- }
- }
-
- private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) {
- if (details == null || !details.hasTransparency) {
- setMedia(uri, Objects.requireNonNull(MediaType.from(contentType)));
- return;
- }
-
- long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
- boolean initiating = threadId == -1;
- SlideDeck slideDeck = new SlideDeck();
-
- if (MediaUtil.isGif(contentType)) {
- slideDeck.addSlide(new GifSlide(requireContext(), uri, 0, details.width, details.height, details.hasTransparency, null));
- } else if (MediaUtil.isImageType(contentType)) {
- slideDeck.addSlide(new ImageSlide(requireContext(), uri, contentType, 0, details.width, details.height, details.hasTransparency, null, null));
- } else {
- throw new AssertionError("Only images are supported!");
- }
-
- sendMediaMessage(recipient.getId(),
- sendButton.getSelectedSendType(),
- "",
- slideDeck,
- null,
- Collections.emptyList(),
- Collections.emptyList(),
- composeText.getMentions(),
- composeText.getStyling(),
- expiresIn,
- false,
- initiating,
- false,
- null);
- }
-
- private class UnverifiedDismissedListener implements UnverifiedBannerView.DismissListener {
- @Override
- public void onDismissed(final List unverifiedIdentities) {
- SimpleTask.run(() -> {
- try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
- for (IdentityRecord identityRecord : unverifiedIdentities) {
- ApplicationDependencies.getProtocolStore().aci().identities().setVerified(identityRecord.getRecipientId(),
- identityRecord.getIdentityKey(),
- VerifiedStatus.DEFAULT);
- }
- }
- return null;
- }, nothing -> initializeIdentityRecords());
- }
- }
-
- private class UnverifiedClickedListener implements UnverifiedBannerView.ClickListener {
- @Override
- public void onClicked(final List unverifiedIdentities) {
- Log.i(TAG, "onClicked: " + unverifiedIdentities.size());
- if (unverifiedIdentities.size() == 1) {
- VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities.get(0), false);
- } else {
- String[] unverifiedNames = new String[unverifiedIdentities.size()];
-
- for (int i=0;i {
- VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireContext(), unverifiedIdentities.get(which), false);
- });
- builder.show();
- }
- }
- }
-
- private final class VoiceNotePlayerViewListener implements VoiceNotePlayerView.Listener {
- @Override
- public void onCloseRequested(@NonNull Uri uri) {
- voiceNoteMediaController.stopPlaybackAndReset(uri);
- }
-
- @Override
- public void onSpeedChangeRequested(@NonNull Uri uri, float speed) {
- voiceNoteMediaController.setPlaybackSpeed(uri, speed);
- }
-
- @Override
- public void onPlay(@NonNull Uri uri, long messageId, double position) {
- voiceNoteMediaController.startSinglePlayback(uri, messageId, position);
- }
-
- @Override
- public void onPause(@NonNull Uri uri) {
- voiceNoteMediaController.pausePlayback(uri);
- }
-
- @Override
- public void onNavigateToMessage(long threadId, @NonNull RecipientId threadRecipientId, @NonNull RecipientId senderId, long messageTimestamp, long messagePositionInThread) {
- if (threadId != ConversationParentFragment.this.threadId) {
- startActivity(ConversationIntents.createBuilderSync(requireActivity(), threadRecipientId, threadId)
- .withStartingPosition((int) messagePositionInThread)
- .build());
- } else {
- fragment.jumpToMessage(senderId, messageTimestamp, () -> { });
- }
- }
- }
-
- private void presentMessageRequestState(@Nullable MessageRequestViewModel.MessageData messageData) {
- if (!Util.isEmpty(viewModel.getArgs().getDraftText()) ||
- viewModel.getArgs().getMedia() != null ||
- viewModel.getArgs().getStickerLocator() != null)
- {
- Log.d(TAG, "[presentMessageRequestState] Have extra, so ignoring provided state.");
- messageRequestBottomView.setVisibility(View.GONE);
- inputPanel.setHideForMessageRequestState(false);
- } else if (isPushGroupV1Conversation() && !isActiveGroup()) {
- Log.d(TAG, "[presentMessageRequestState] Inactive push group V1, so ignoring provided state.");
- messageRequestBottomView.setVisibility(View.GONE);
- inputPanel.setHideForMessageRequestState(false);
- } else if (messageData == null) {
- Log.d(TAG, "[presentMessageRequestState] Null messageData. Ignoring.");
- } else if (messageData.getMessageState() == MessageRequestState.NONE) {
- Log.d(TAG, "[presentMessageRequestState] No message request necessary.");
- messageRequestBottomView.setVisibility(View.GONE);
- inputPanel.setHideForMessageRequestState(false);
- } else {
- Log.d(TAG, "[presentMessageRequestState] " + messageData.getMessageState());
- messageRequestBottomView.setMessageData(messageData);
- messageRequestBottomView.setVisibility(View.VISIBLE);
- noLongerMemberBanner.setVisibility(View.GONE);
- inputPanel.setHideForMessageRequestState(true);
- }
-
- invalidateOptionsMenu();
- }
-
- private static class KeyboardImageDetails {
- private final int width;
- private final int height;
- private final boolean hasTransparency;
-
- private KeyboardImageDetails(int width, int height, boolean hasTransparency) {
- this.width = width;
- this.height = height;
- this.hasTransparency = hasTransparency;
- }
- }
-
- public interface Callback {
- long getShareDataTimestamp();
-
- void setShareDataTimestamp(long timestamp);
-
- default void onInitializeToolbar(@NonNull Toolbar toolbar) {
- }
-
- default void onSendComplete(long threadId) {
- }
-
- /**
- * @return true to skip built in, otherwise false.
- */
- default boolean onUpdateReminders() {
- return false;
- }
-
- default boolean isInBubble() {
- return false;
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java
deleted file mode 100644
index a21e70eae0..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.kt
new file mode 100644
index 0000000000..ce6aea3b8e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
index 83292a4aaa..d820749032 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java
@@ -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);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
index 431eca7574..885834782c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java
@@ -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 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 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 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 resolveMessageToEdit(@NonNull ConversationMessage message) {
return Single.fromCallable(() -> {
@@ -223,42 +153,4 @@ public class ConversationRepository {
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
-
- Observable getUnreadCount(long threadId, long afterTime) {
- if (threadId <= -1L || afterTime <= 0L) {
- return Observable.just(0);
- }
-
- return Observable. 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));
- }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java
deleted file mode 100644
index d13fd12f5a..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationStickerViewModel.java
+++ /dev/null
@@ -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> stickers;
- private final MutableLiveData 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> getStickerResults() {
- return stickers;
- }
-
- @NonNull LiveData 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 create(@NonNull Class modelClass) {
- //noinspection ConstantConditions
- return modelClass.cast(new ConversationStickerViewModel(application, repository));
- }
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
deleted file mode 100644
index 0eed4dc0a4..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java
+++ /dev/null
@@ -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> recentMedia;
- private final BehaviorSubject threadId;
- private final Observable messageData;
- private final MutableLiveData showScrollButtons;
- private final MutableLiveData hasUnreadMentions;
- private final Observable canShowAsBubble;
- private final ProxyPagingController pagingController;
- private final DatabaseObserver.Observer conversationObserver;
- private final DatabaseObserver.MessageObserver messageUpdateObserver;
- private final DatabaseObserver.MessageObserver messageInsertObserver;
- private final BehaviorSubject recipientId;
- private final Observable> wallpaper;
- private final SingleLiveEvent events;
- private final Observable chatColors;
- private final MutableLiveData toolbarBottom;
- private final MutableLiveData inlinePlayerHeight;
- private final LiveData conversationTopMargin;
- private final Store threadAnimationStateStore;
- private final Observer threadAnimationStateStoreDriver;
- private final NotificationProfilesRepository notificationProfilesRepository;
- private final MutableLiveData searchQuery;
- private final GroupAuthorNameColorHelper groupAuthorNameColorHelper;
- private final RxStore conversationStateStore;
- private final CompositeDisposable disposables;
- private final BehaviorSubject conversationStateTick;
- private final PublishProcessor markReadRequestPublisher;
- private final Observable 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 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 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 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 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 getStoryViewState() {
- return recipientId
- .subscribeOn(Schedulers.io())
- .switchMap(StoryViewState::getForRecipientId)
- .distinctUntilChanged()
- .observeOn(AndroidSchedulers.mainThread());
- }
-
- void onMessagesCommitted(@NonNull List 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 getMarkReadRequests() {
- return markReadRequestPublisher.onBackpressureBuffer();
- }
-
- @NonNull Observable getThreadUnreadCount(long afterTime) {
- return threadId.switchMap(id -> conversationRepository.getUnreadCount(id, afterTime));
- }
-
- @NonNull Flowable getConversationState() {
- return conversationStateStore.getStateFlowable().observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull Flowable 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 getSearchQuery() {
- return searchQuery;
- }
-
- @NonNull LiveData getConversationTopMargin() {
- return conversationTopMargin;
- }
-
- @NonNull Observable canShowAsBubble() {
- return canShowAsBubble
- .observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull LiveData getShowScrollToBottom() {
- return Transformations.distinctUntilChanged(showScrollButtons);
- }
-
- @NonNull LiveData getShowMentionsButton() {
- return Transformations.distinctUntilChanged(LiveDataUtil.combineLatest(showScrollButtons, hasUnreadMentions, (a, b) -> a && b));
- }
-
- @NonNull Observable> getWallpaper() {
- return wallpaper
- .observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull LiveData getEvents() {
- return events;
- }
-
- @NonNull Observable getChatColors() {
- return chatColors
- .observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull Observable 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> getRecentMedia() {
- return recentMedia;
- }
-
- @NonNull Observable getMessageData() {
- return messageData
- .observeOn(AndroidSchedulers.mainThread());
- }
-
- @NonNull PagingController getPagingController() {
- return pagingController;
- }
-
- @NonNull Observable