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 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 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 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 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 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> getNameColorsMap() { - return recipientId - .observeOn(Schedulers.io()) - .distinctUntilChanged() - .map(Recipient::resolved) - .map(recipient -> { - if (recipient.getGroupId().isPresent()) { - return groupAuthorNameColorHelper.getColorMap(recipient.getGroupId().get()); - } else { - return Collections.emptyMap(); - } - }) - .observeOn(AndroidSchedulers.mainThread()); - } - - @NonNull LiveData> getActiveNotificationProfile() { - Flowable> activeProfile = notificationProfilesRepository.getProfiles() - .map(profiles -> Optional.ofNullable(NotificationProfiles.getActiveProfile(profiles))); - - return LiveDataReactiveStreams.fromPublisher(activeProfile); - } - - @NonNull - public Single resolveMessageToEdit(@NonNull ConversationMessage message) { - return conversationRepository.resolveMessageToEdit(message); - } - - void setArgs(@NonNull ConversationIntents.Args args) { - this.args = args; - } - - @NonNull ConversationIntents.Args getArgs() { - return Objects.requireNonNull(args); - } - - @Subscribe(threadMode = ThreadMode.POSTING) - public void onRecaptchaRequiredEvent(@NonNull RecaptchaRequiredEvent event) { - events.postValue(Event.SHOW_RECAPTCHA); - } - - @Override - protected void onCleared() { - super.onCleared(); - threadAnimationStateStore.getStateLiveData().removeObserver(threadAnimationStateStoreDriver); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(conversationObserver); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageUpdateObserver); - ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageInsertObserver); - disposables.clear(); - conversationStateStore.dispose(); - EventBus.getDefault().unregister(this); - } - - public void insertSmsExportUpdateEvent(@NonNull Recipient recipient) { - conversationRepository.insertSmsExportUpdateEvent(recipient); - } - - enum Event { - SHOW_RECAPTCHA - } - - static class MessageData { - private final List messages; - private final ConversationData metadata; - - MessageData(@NonNull ConversationData metadata, @NonNull List messages) { - this.metadata = metadata; - this.messages = messages; - } - - public @NonNull List getMessages() { - return messages; - } - - public @NonNull ConversationData getMetadata() { - return metadata; - } - } - - static class Factory extends ViewModelProvider.NewInstanceFactory { - @Override - public @NonNull T create(@NonNull Class modelClass) { - //noinspection ConstantConditions - return modelClass.cast(new ConversationViewModel()); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/LastSeenHeader.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/LastSeenHeader.java deleted file mode 100644 index 49c796f797..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/LastSeenHeader.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder; -import org.thoughtcrime.securesms.util.StickyHeaderDecoration; - -class LastSeenHeader extends StickyHeaderDecoration { - - private final ConversationAdapter adapter; - private final long lastSeenTimestamp; - - private long unreadCount; - - LastSeenHeader(ConversationAdapter adapter, long lastSeenTimestamp) { - super(adapter, false, false, ConversationAdapter.HEADER_TYPE_LAST_SEEN); - this.adapter = adapter; - this.lastSeenTimestamp = lastSeenTimestamp; - } - - public void setUnreadCount(long unreadCount) { - this.unreadCount = unreadCount; - } - - @Override - protected boolean hasHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - if (lastSeenTimestamp <= 0 || unreadCount <= 0) { - return false; - } - - long currentRecordTimestamp = adapter.getReceivedTimestamp(position); - long previousRecordTimestamp = adapter.getReceivedTimestamp(position + 1); - - return currentRecordTimestamp > lastSeenTimestamp && previousRecordTimestamp < lastSeenTimestamp; - } - - @Override - protected int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, int layoutPos) { - return parent.getLayoutManager().getDecoratedTop(child); - } - - @Override - protected @NonNull RecyclerView.ViewHolder getHeader(RecyclerView parent, StickyHeaderAdapter stickyAdapter, int position) { - StickyHeaderViewHolder viewHolder = new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_last_seen, parent, false)); - adapter.onBindLastSeenViewHolder(viewHolder, unreadCount); - - int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); - int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); - - int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), viewHolder.itemView.getLayoutParams().width); - int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), viewHolder.itemView.getLayoutParams().height); - - viewHolder.itemView.measure(childWidth, childHeight); - viewHolder.itemView.layout(0, 0, viewHolder.itemView.getMeasuredWidth(), viewHolder.itemView.getMeasuredHeight()); - - return viewHolder; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java deleted file mode 100644 index 39ab69b8af..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MessageCountsViewModel.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; -import androidx.lifecycle.ViewModel; - -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.libsignal.protocol.util.Pair; -import org.thoughtcrime.securesms.database.DatabaseObserver; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.model.ThreadRecord; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor; - -import java.util.concurrent.Executor; - -public class MessageCountsViewModel extends ViewModel { - - private static final Executor EXECUTOR = new SerialMonoLifoExecutor(SignalExecutors.BOUNDED); - - private final Application context; - private final MutableLiveData threadId = new MutableLiveData<>(-1L); - private final LiveData> unreadCounts; - - private DatabaseObserver.Observer observer; - - public MessageCountsViewModel() { - this.context = ApplicationDependencies.getApplication(); - this.unreadCounts = Transformations.switchMap(Transformations.distinctUntilChanged(threadId), id -> { - - MutableLiveData> counts = new MutableLiveData<>(new Pair<>(0, 0)); - - if (id == -1L) { - return counts; - } - - if (observer != null) { - ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer); - } - - observer = new DatabaseObserver.Observer() { - private int previousUnreadCount = -1; - - @Override - public void onChanged() { - EXECUTOR.execute(() -> { - int unreadCount = getUnreadCount(context, id); - if (unreadCount != previousUnreadCount) { - previousUnreadCount = unreadCount; - counts.postValue(new Pair<>(unreadCount, getUnreadMentionsCount(context, id))); - } - }); - } - }; - - observer.onChanged(); - - ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer); - - return counts; - }); - - } - - void setThreadId(long threadId) { - this.threadId.setValue(threadId); - } - - void clearThreadId() { - this.threadId.postValue(-1L); - } - - @NonNull LiveData getUnreadMessagesCount() { - return Transformations.map(unreadCounts, Pair::first); - } - - @NonNull LiveData getUnreadMentionsCount() { - return Transformations.map(unreadCounts, Pair::second); - } - - private int getUnreadCount(@NonNull Context context, long threadId) { - ThreadRecord threadRecord = SignalDatabase.threads().getThreadRecord(threadId); - return threadRecord != null ? threadRecord.getUnreadCount() : 0; - } - - private int getUnreadMentionsCount(@NonNull Context context, long threadId) { - return SignalDatabase.messages().getUnreadMentionCount(threadId); - } - - @Override - protected void onCleared() { - if (observer != null) { - ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt index 363b8c2576..ba3efd5063 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftRepository.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.text.Spannable import android.text.SpannableString import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.StreamUtil import org.signal.core.util.concurrent.MaybeCompat @@ -167,22 +166,16 @@ class DraftRepository( } } - fun saveDrafts(recipient: Recipient?, threadId: Long, distributionType: Int, drafts: Drafts) { - require(threadId != -1L || recipient != null) + fun saveDrafts(threadId: Long, drafts: Drafts) { + require(threadId != -1L) saveDraftsExecutor.execute { if (drafts.isNotEmpty()) { - val actualThreadId = if (threadId == -1L) { - threadTable.getOrCreateThreadIdFor(recipient!!, distributionType) - } else { - threadId - } - - draftTable.replaceDrafts(actualThreadId, drafts) + draftTable.replaceDrafts(threadId, drafts) if (drafts.shouldUpdateSnippet()) { - threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true) + threadTable.updateSnippet(threadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true) } else { - threadTable.update(actualThreadId, unarchive = false, allowDeletion = false) + threadTable.update(threadId, unarchive = false, allowDeletion = false) } } else if (threadId > 0) { draftTable.clearDrafts(threadId) @@ -191,13 +184,6 @@ class DraftRepository( } } - @Deprecated("Not needed for CFv2") - fun loadDrafts(threadId: Long): Single { - return Single.fromCallable { - loadDraftsInternal(threadId) - }.subscribeOn(Schedulers.io()) - } - private fun loadDraftsInternal(threadId: Long): DatabaseDraft { val drafts: Drafts = draftTable.getDrafts(threadId) val bodyRangesDraft: DraftTable.Draft? = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES) @@ -218,11 +204,6 @@ class DraftRepository( return DatabaseDraft(drafts, updatedText) } - @Deprecated("Not needed for CFv2") - fun loadDraftQuote(serialized: String): Maybe { - return MaybeCompat.fromCallable { loadDraftQuoteInternal(serialized) } - } - private fun loadDraftQuoteInternal(serialized: String): ConversationMessage? { val quoteId: QuoteId = QuoteId.deserialize(context, serialized) ?: return null val messageRecord: MessageRecord = SignalDatabase.messages.getMessageFor(quoteId.id, quoteId.author)?.let { @@ -237,11 +218,6 @@ class DraftRepository( return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, threadRecipient) } - @Deprecated("Not needed for CFv2") - fun loadDraftMessageEdit(serialized: String): Maybe { - return MaybeCompat.fromCallable { loadDraftMessageEditInternal(serialized) } - } - private fun loadDraftMessageEditInternal(serialized: String): ConversationMessage? { val messageId = MessageId.deserialize(serialized) val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftState.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftState.kt index 4796dc3d42..14c18f1468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftState.kt @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation.drafts import org.thoughtcrime.securesms.database.DraftTable import org.thoughtcrime.securesms.database.DraftTable.Drafts -import org.thoughtcrime.securesms.recipients.RecipientId /** * State object responsible for holding Voice Note draft state. The intention is to allow @@ -10,11 +9,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId * management pattern going forward for drafts. */ data class DraftState( - @Deprecated("Not needed for CFv2") - val recipientId: RecipientId? = null, val threadId: Long = -1, - @Deprecated("Not needed for CFv2") - val distributionType: Int = 0, val textDraft: DraftTable.Draft? = null, val bodyRangesDraft: DraftTable.Draft? = null, val quoteDraft: DraftTable.Draft? = null, @@ -24,7 +19,7 @@ data class DraftState( ) { fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState { - return DraftState(recipientId = recipientId, threadId = threadId, distributionType = distributionType) + return DraftState(threadId = threadId) } fun toDrafts(): Drafts { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt index f171c08220..7cde31af0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/drafts/DraftViewModel.kt @@ -4,17 +4,13 @@ import androidx.lifecycle.ViewModel import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.components.location.SignalPlace -import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.database.DraftTable.Draft import org.thoughtcrime.securesms.database.MentionUtil import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.mms.QuoteId -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.rx.RxStore @@ -40,16 +36,6 @@ class DraftViewModel @JvmOverloads constructor( store.dispose() } - @Deprecated("Not needed for CFv2") - fun setThreadId(threadId: Long) { - store.update { it.copy(threadId = threadId) } - } - - @Deprecated("Not needed for CFv2") - fun setDistributionType(distributionType: Int) { - store.update { it.copy(distributionType = distributionType) } - } - fun saveEphemeralVoiceNoteDraft(draft: Draft) { store.update { draftState -> saveDrafts(draftState.copy(voiceNoteDraft = draft)) @@ -67,11 +53,6 @@ class DraftViewModel @JvmOverloads constructor( } } - @Deprecated("Not needed for CFv2") - fun onRecipientChanged(recipient: Recipient) { - store.update { it.copy(recipientId = recipient.id) } - } - fun setMessageEditDraft(messageId: MessageId, text: String, mentions: List, styleBodyRanges: BodyRangeList?) { store.update { val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions) @@ -140,34 +121,10 @@ class DraftViewModel @JvmOverloads constructor( } private fun saveDrafts(state: DraftState): DraftState { - repository.saveDrafts(state.recipientId?.let { Recipient.resolved(it) }, state.threadId, state.distributionType, state.toDrafts()) + repository.saveDrafts(state.threadId, state.toDrafts()) return state } - @Deprecated("Not needed for CFv2") - fun loadDrafts(threadId: Long): Single { - return repository - .loadDrafts(threadId) - .doOnSuccess { drafts -> - store.update { saveDrafts(it.copyAndSetDrafts(threadId, drafts.drafts)) } - } - .observeOn(AndroidSchedulers.mainThread()) - } - - @Deprecated("Not needed for CFv2") - fun loadDraftQuote(serialized: String): Maybe { - return repository.loadDraftQuote(serialized) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - } - - @Deprecated("Not needed for CFv2") - fun loadDraftEditMessage(serialized: String): Maybe { - return repository.loadDraftMessageEdit(serialized) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - } - fun loadShareOrDraftData(lastShareDataTimestamp: Long): Maybe { return repository.getShareOrDraftData(lastShareDataTimestamp) .doOnSuccess { (_, drafts) -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt index 26e83afd6f..5827405b8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/MultiselectItemDecoration.kt @@ -36,7 +36,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge.PulseRequest import org.thoughtcrime.securesms.conversation.v2.items.InteractiveConversationElement import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.wallpaper.ChatWallpaper @@ -402,12 +401,6 @@ class MultiselectItemDecoration( } } } - - if (!FeatureFlags.useConversationFragmentV2()) { - canvas.clipPath(path) - canvas.drawShade() - canvas.restore() - } } } @@ -422,12 +415,6 @@ class MultiselectItemDecoration( path.addRect(child.left.toFloat(), child.top.toFloat(), child.right.toFloat(), child.bottom.toFloat(), Path.Direction.CW) } } - - if (!FeatureFlags.useConversationFragmentV2()) { - canvas.clipPath(path, Region.Op.DIFFERENCE) - canvas.drawShade() - canvas.restore() - } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/EnableCallNotificationSettingsDialog.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/EnableCallNotificationSettingsDialog.java index 248b832195..b326b55cbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/EnableCallNotificationSettingsDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/EnableCallNotificationSettingsDialog.java @@ -25,7 +25,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.conversation.ConversationFragment; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.util.DeviceProperties; @@ -123,8 +122,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment { @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); - if (getParentFragment() instanceof ConversationFragment) { - ((ConversationFragment) getParentFragment()).refreshList(); + if (getParentFragment() instanceof Callback) { + ((Callback) getParentFragment()).onCallNotificationSettingsDialogDismissed(); } } @@ -230,4 +229,8 @@ public final class EnableCallNotificationSettingsDialog extends DialogFragment { return bitmask; } + + public interface Callback { + void onCallNotificationSettingsDialogDismissed(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java deleted file mode 100644 index 3487ca48c0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/groupcall/GroupCallViewModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.thoughtcrime.securesms.conversation.ui.groupcall; - -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.events.GroupCallPeekEvent; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; - -import java.util.Objects; - -public class GroupCallViewModel extends ViewModel { - - private static final String TAG = Log.tag(GroupCallViewModel.class); - - private final MutableLiveData activeGroup; - private final MutableLiveData ongoingGroupCall; - private final LiveData activeGroupCall; - private final MutableLiveData groupCallHasCapacity; - - private @Nullable Recipient currentRecipient; - - GroupCallViewModel() { - this.activeGroup = new MutableLiveData<>(false); - this.ongoingGroupCall = new MutableLiveData<>(false); - this.groupCallHasCapacity = new MutableLiveData<>(false); - this.activeGroupCall = LiveDataUtil.combineLatest(activeGroup, ongoingGroupCall, (active, ongoing) -> active && ongoing); - } - - public @NonNull LiveData hasActiveGroupCall() { - return activeGroupCall; - } - - public @NonNull LiveData groupCallHasCapacity() { - return groupCallHasCapacity; - } - - public void onRecipientChange(@Nullable Recipient recipient) { - activeGroup.postValue(recipient != null && recipient.isActiveGroup()); - - if (Objects.equals(currentRecipient, recipient)) { - return; - } - - ongoingGroupCall.postValue(false); - groupCallHasCapacity.postValue(false); - - currentRecipient = recipient; - - peekGroupCall(); - } - - public void peekGroupCall() { - if (isGroupCallCapable(currentRecipient)) { - Log.i(TAG, "peek call for " + currentRecipient.getId()); - ApplicationDependencies.getSignalCallManager().peekGroupCall(currentRecipient.getId()); - } - } - - public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) { - if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) { - Log.i(TAG, "update UI with call event: ongoing call: " + groupCallPeekEvent.isOngoing() + " hasCapacity: " + groupCallPeekEvent.callHasCapacity()); - - ongoingGroupCall.postValue(groupCallPeekEvent.isOngoing()); - groupCallHasCapacity.postValue(groupCallPeekEvent.callHasCapacity()); - } else { - Log.i(TAG, "Ignore call event for different recipient."); - } - } - - private static boolean isGroupCallCapable(@Nullable Recipient recipient) { - return recipient != null && recipient.isActiveGroup() && recipient.isPushV2Group() && Build.VERSION.SDK_INT > 19; - } - - public static final class Factory implements ViewModelProvider.Factory { - @Override - public @NonNull T create(@NonNull Class modelClass) { - //noinspection ConstantConditions - return modelClass.cast(new GroupCallViewModel()); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt index 61b8cc25e3..cdfa42b059 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner +import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme import java.util.concurrent.TimeUnit @@ -20,7 +21,7 @@ import java.util.concurrent.TimeUnit /** * Wrapper activity for ConversationFragment. */ -class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent { +open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent { companion object { private const val STATE_WATERMARK = "share_data_watermark" @@ -88,7 +89,11 @@ class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControl private fun replaceFragment() { val fragment = ConversationFragment().apply { - arguments = intent.extras + arguments = if (ConversationIntents.isBubbleIntentUri(intent.data)) { + ConversationIntents.createParentFragmentArguments(intent) + } else { + intent.extras + } } supportFragmentManager diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index f3be576211..3594a33736 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -120,6 +120,7 @@ import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity +import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState @@ -331,7 +332,8 @@ class ConversationFragment : ScheduleMessageTimePickerBottomSheet.ScheduleCallback, ScheduleMessageDialogCallback, ConversationBottomSheetCallback, - SafetyNumberBottomSheet.Callbacks { + SafetyNumberBottomSheet.Callbacks, + EnableCallNotificationSettingsDialog.Callback { companion object { private val TAG = Log.tag(ConversationFragment::class.java) @@ -458,7 +460,6 @@ class ConversationFragment : private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration private lateinit var conversationItemDecorations: ConversationItemDecorations private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback - private lateinit var menuProvider: ConversationOptionsMenu.Provider private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration private lateinit var backPressedCallback: BackPressedDelegate @@ -471,6 +472,7 @@ class ConversationFragment : private var reShowScheduleMessagesBar: Boolean = false private var composeTextEventsListener: ComposeTextEventsListener? = null private var dataObserver: DataObserver? = null + private var menuProvider: ConversationOptionsMenu.Provider? = null private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy { override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) { @@ -559,6 +561,8 @@ class ConversationFragment : binding.conversationVideoContainer.setClipToOutline(true) + SpoilerAnnotation.resetRevealedSpoilers() + registerForResults() } @@ -761,6 +765,10 @@ class ConversationFragment : override fun onCanceled() = Unit + override fun onCallNotificationSettingsDialogDismissed() { + adapter.notifyDataSetChanged() + } + //endregion private fun observeConversationThread() { @@ -813,7 +821,7 @@ class ConversationFragment : backPressedCallback = BackPressedDelegate() requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback) - menuProvider.afterFirstRenderMode = true + menuProvider?.afterFirstRenderMode = true viewLifecycleOwner.lifecycle.addObserver(LastScrolledPositionUpdater(adapter, layoutManager, viewModel)) @@ -858,7 +866,7 @@ class ConversationFragment : setOnClickListener(sendButtonListener) setScheduledSendListener(sendButtonListener) isEnabled = true - post { sendButton.triggerSelectedChangedEvent() } + sendButton.triggerSelectedChangedEvent() } sendEditButton.setOnClickListener { handleSendEditMessage() } @@ -981,6 +989,11 @@ class ConversationFragment : val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() } viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick) + + if (args.conversationScreenType.isInPopup) { + composeText.requestFocus() + binding.conversationInputPanel.quickAttachmentToggle.disable() + } } private fun initializeInlineSearch() { @@ -1162,15 +1175,17 @@ class ConversationFragment : } private fun presentActionBarMenu() { - optionsMenuCallback = ConversationOptionsMenuCallback() - menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables) - binding.toolbar.addMenuProvider(menuProvider) - invalidateOptionsMenu() + if (!args.conversationScreenType.isInPopup) { + optionsMenuCallback = ConversationOptionsMenuCallback() + menuProvider = ConversationOptionsMenu.Provider(optionsMenuCallback, disposables) + binding.toolbar.addMenuProvider(menuProvider!!) + invalidateOptionsMenu() + } when (args.conversationScreenType) { ConversationScreenType.NORMAL -> presentNavigationIconForNormal() - ConversationScreenType.BUBBLE -> presentNavigationIconForBubble() - ConversationScreenType.POPUP -> Unit + ConversationScreenType.BUBBLE, + ConversationScreenType.POPUP -> presentNavigationIconForBubble() } } @@ -1209,8 +1224,10 @@ class ConversationFragment : titleView.clearExpiring() } - titleView.setOnClickListener { - optionsMenuCallback.handleConversationSettings() + if (!args.conversationScreenType.isInPopup) { + titleView.setOnClickListener { + optionsMenuCallback.handleConversationSettings() + } } } @@ -1751,7 +1768,9 @@ class ConversationFragment : disposables += send .doOnSubscribe { if (clearCompose) { + composeTextEventsListener?.typingStatusEnabled = false composeText.setText("") + composeTextEventsListener?.typingStatusEnabled = true attachmentManager.clear(GlideApp.with(this@ConversationFragment), false) inputPanel.clearQuote() } @@ -1767,9 +1786,6 @@ class ConversationFragment : private fun onSendComplete() { if (isDetached || activity?.isFinishing == true) { - if (args.conversationScreenType.isInPopup) { - activity?.finish() - } return } @@ -1781,6 +1797,10 @@ class ConversationFragment : draftViewModel.onSendComplete() inputPanel.exitEditMessageMode() + + if (args.conversationScreenType.isInPopup) { + activity?.finish() + } } private fun handleRecentSafetyNumberChange(changedRecords: List) { @@ -2004,6 +2024,7 @@ class ConversationFragment : } else if (isSearchRequested) { searchMenuItem?.collapseActionView() } else if (args.conversationScreenType.isInBubble) { + isEnabled = false requireActivity().onBackPressed() } else { requireActivity().finish() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt index e936aee939..394b080121 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/ConversationDataSource.kt @@ -11,7 +11,6 @@ import org.signal.core.util.logging.Log import org.signal.core.util.toInt import org.signal.paging.PagedDataSource import org.thoughtcrime.securesms.conversation.ConversationData -import org.thoughtcrime.securesms.conversation.ConversationDataSource import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory import org.thoughtcrime.securesms.database.MessageTable diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index de92d57abd..d9bb8d6059 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -123,7 +123,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator; import org.thoughtcrime.securesms.contacts.paged.ContactSearchState; import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet; import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet; -import org.thoughtcrime.securesms.conversation.ConversationFragment; import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest; import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterSource; import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationListFilterPullView; @@ -169,6 +168,7 @@ import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.ConversationUtil; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.SignalLocalMetrics; @@ -979,8 +979,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode requireCallback().getSearchToolbar().get(); } - if (getContext() != null) { - ConversationFragment.prepare(getContext()); + Context context = getContext(); + if (context != null) { + FrameLayout parent = new FrameLayout(context); + parent.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); + + if (SignalStore.internalValues().useConversationItemV2()) { + CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_incoming, parent, 25); + CachedInflater.from(context).cacheUntilLimit(R.layout.v2_conversation_item_text_only_outgoing, parent, 25); + } else { + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_text_only, parent, 25); + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_text_only, parent, 25); + } + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_received_multimedia, parent, 10); + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_sent_multimedia, parent, 10); + CachedInflater.from(context).cacheUntilLimit(R.layout.conversation_item_update, parent, 5); + CachedInflater.from(context).cacheUntilLimit(R.layout.cursor_adapter_header_footer_view, parent, 2); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt index bd4f901856..483cea1609 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt @@ -4,8 +4,7 @@ import android.graphics.Canvas import android.graphics.Rect import androidx.core.view.children import androidx.recyclerview.widget.RecyclerView -import org.thoughtcrime.securesms.conversation.ConversationAdapter -import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2 +import org.thoughtcrime.securesms.conversation.ConversationHeaderView import kotlin.math.min /** @@ -28,28 +27,22 @@ class GiphyMp4ItemDecoration( parent.translationY = 0f onRecyclerVerticalTranslationSet?.invoke(parent.translationY) } else { - val threadHeaderViewHolder = parent.children - .map { parent.getChildViewHolder(it) } - .filter { it is ConversationAdapter.FooterViewHolder || it is ConversationAdapterV2.ThreadHeaderViewHolder } + val threadHeaderView: ConversationHeaderView? = parent.children + .filterIsInstance() .firstOrNull() - if (threadHeaderViewHolder == null) { + if (threadHeaderView == null) { parent.translationY = 0f onRecyclerVerticalTranslationSet?.invoke(parent.translationY) return } - val toolbarMargin = if (threadHeaderViewHolder is ConversationAdapterV2.ThreadHeaderViewHolder) { - // A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height - val bounds = Rect() - parent.getDecoratedBoundsWithMargins(threadHeaderViewHolder.itemView, bounds) - bounds.bottom - bounds.top - threadHeaderViewHolder.itemView.height - } else { - // Deprecated not needed for CFv2 - 0 - } + // A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height + val bounds = Rect() + parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds) + val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height - val childTop: Int = threadHeaderViewHolder.itemView.top - toolbarMargin + val childTop: Int = threadHeaderView.top - toolbarMargin parent.translationY = min(0, -childTop).toFloat() onRecyclerVerticalTranslationSet?.invoke(parent.translationY) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index ef4ba68f70..35cb73c53d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -107,7 +107,6 @@ public final class FeatureFlags { private static final String MAX_ATTACHMENT_SIZE_BYTES = "global.attachments.maxBytes"; private static final String SVR2_KILLSWITCH = "android.svr2.killSwitch"; private static final String CDS_DISABLE_COMPAT_MODE = "cds.disableCompatibilityMode"; - private static final String CONVERSATION_FRAGMENT_V2 = "android.conversationFragmentV2.2"; private static final String FCM_MAY_HAVE_MESSAGES_KILL_SWITCH = "android.fcmNotificationFallbackKillSwitch"; private static final String SAFETY_NUMBER_ACI = "global.safetyNumberAci"; @@ -169,7 +168,6 @@ public final class FeatureFlags { AD_HOC_CALLING, SVR2_KILLSWITCH, CDS_DISABLE_COMPAT_MODE, - CONVERSATION_FRAGMENT_V2, SAFETY_NUMBER_ACI, FCM_MAY_HAVE_MESSAGES_KILL_SWITCH ); @@ -237,7 +235,6 @@ public final class FeatureFlags { MAX_ATTACHMENT_SIZE_BYTES, SVR2_KILLSWITCH, CDS_DISABLE_COMPAT_MODE, - CONVERSATION_FRAGMENT_V2, SAFETY_NUMBER_ACI, FCM_MAY_HAVE_MESSAGES_KILL_SWITCH ); @@ -621,11 +618,6 @@ public final class FeatureFlags { } } - /** True if the new conversation fragment should be used. */ - public static boolean useConversationFragmentV2() { - return getBoolean(CONVERSATION_FRAGMENT_V2, false); - } - /** Only for rendering debug info. */ public static synchronized @NonNull Map getMemoryValues() { return new TreeMap<>(REMOTE_VALUES); diff --git a/app/src/main/res/layout/conversation_activity.xml b/app/src/main/res/layout/conversation_activity.xml deleted file mode 100644 index 5905eff7bd..0000000000 --- a/app/src/main/res/layout/conversation_activity.xml +++ /dev/null @@ -1,302 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -