mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add rendering and handling for various disabled input states in CFv2.
This commit is contained in:
@@ -4275,24 +4275,10 @@ public class ConversationParentFragment extends Fragment
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setNeutralButton(R.string.ConversationActivity_cancel, (d, w) -> d.dismiss());
|
||||
|
||||
if (recipient.isGroup() && recipient.isBlocked()) {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_conversation);
|
||||
builder.setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setPositiveButton(R.string.ConversationActivity_delete, (d, w) -> requestModel.onDelete());
|
||||
} else if (recipient.isGroup()) {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_and_leave_group);
|
||||
builder.setMessage(R.string.ConversationActivity_you_will_leave_this_group_and_it_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setNegativeButton(R.string.ConversationActivity_delete_and_leave, (d, w) -> requestModel.onDelete());
|
||||
} else {
|
||||
builder.setTitle(R.string.ConversationActivity_delete_conversation);
|
||||
builder.setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices);
|
||||
builder.setNegativeButton(R.string.ConversationActivity_delete, (d, w) -> requestModel.onDelete());
|
||||
}
|
||||
|
||||
builder.show();
|
||||
ConversationDialogs.displayDeleteDialog(requireContext(), recipient, () -> {
|
||||
requestModel.onDelete();
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void onMessageRequestBlockClicked(@NonNull MessageRequestViewModel requestModel) {
|
||||
|
||||
@@ -112,4 +112,26 @@ object ConversationDialogs {
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun displayDeleteDialog(context: Context, recipient: Recipient, onDelete: () -> Unit) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setNeutralButton(R.string.ConversationActivity_cancel, null)
|
||||
.apply {
|
||||
if (recipient.isGroup && recipient.isBlocked) {
|
||||
setTitle(R.string.ConversationActivity_delete_conversation)
|
||||
setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices)
|
||||
setPositiveButton(R.string.ConversationActivity_delete) { _, _ -> onDelete() }
|
||||
} else if (recipient.isGroup) {
|
||||
setTitle(R.string.ConversationActivity_delete_and_leave_group)
|
||||
setMessage(R.string.ConversationActivity_you_will_leave_this_group_and_it_will_be_deleted_from_all_of_your_devices)
|
||||
setNegativeButton(R.string.ConversationActivity_delete_and_leave) { _, _ -> onDelete() }
|
||||
} else {
|
||||
setTitle(R.string.ConversationActivity_delete_conversation)
|
||||
setMessage(R.string.ConversationActivity_this_conversation_will_be_deleted_from_all_of_your_devices)
|
||||
setNegativeButton(R.string.ConversationActivity_delete) { _, _ -> onDelete() }
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +46,18 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -86,6 +90,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu
|
||||
import org.thoughtcrime.securesms.conversation.MarkReadHelper
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.conversation.ShowAdminsBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
@@ -112,10 +117,12 @@ 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.GroupChangeFailureReason
|
||||
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.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
|
||||
import org.thoughtcrime.securesms.invites.InviteActions
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -127,6 +134,8 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
@@ -139,6 +148,7 @@ import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity
|
||||
@@ -149,11 +159,13 @@ import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.thoughtcrime.securesms.util.WindowUtil
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil
|
||||
@@ -176,17 +188,21 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
ConversationRecipientRepository(args.threadId)
|
||||
}
|
||||
|
||||
private val messageRequestRepository: MessageRequestRepository by lazy {
|
||||
MessageRequestRepository(requireContext())
|
||||
}
|
||||
|
||||
private val disposables = LifecycleDisposable()
|
||||
private val binding by ViewBinderDelegate(V2ConversationFragmentBinding::bind)
|
||||
private val viewModel: ConversationViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
ConversationViewModel.Factory(
|
||||
args,
|
||||
ConversationRepository(requireContext()),
|
||||
conversationRecipientRepository
|
||||
)
|
||||
}
|
||||
)
|
||||
private val viewModel: ConversationViewModel by viewModel {
|
||||
ConversationViewModel(
|
||||
args.threadId,
|
||||
args.startingPosition,
|
||||
ConversationRepository(requireContext()),
|
||||
conversationRecipientRepository,
|
||||
messageRequestRepository
|
||||
)
|
||||
}
|
||||
|
||||
private val groupCallViewModel: ConversationGroupCallViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
@@ -200,6 +216,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
)
|
||||
|
||||
private val messageRequestViewModel: MessageRequestViewModel by viewModel {
|
||||
MessageRequestViewModel(args.threadId, conversationRecipientRepository, messageRequestRepository)
|
||||
}
|
||||
|
||||
private val conversationTooltips = ConversationTooltips(this)
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
@@ -264,6 +284,13 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
|
||||
observeConversationThread()
|
||||
|
||||
viewModel
|
||||
.inputReadyState
|
||||
.subscribeBy(
|
||||
onNext = this::presentInputReadyState
|
||||
)
|
||||
.addTo(disposables)
|
||||
|
||||
container.fragmentManager = childFragmentManager
|
||||
}
|
||||
|
||||
@@ -346,6 +373,9 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount)
|
||||
})
|
||||
|
||||
val disabledInputListener = DisabledInputListener()
|
||||
binding.conversationDisabledInput.listener = disabledInputListener
|
||||
|
||||
val sendButtonListener = SendButtonListener()
|
||||
val composeTextEventsListener = ComposeTextEventsListener()
|
||||
|
||||
@@ -397,6 +427,28 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
childFragmentManager.setFragmentResultListener(AttachmentKeyboardFragment.RESULT_KEY, viewLifecycleOwner, AttachmentKeyboardFragmentListener())
|
||||
}
|
||||
|
||||
private fun presentInputReadyState(inputReadyState: InputReadyState) {
|
||||
val disabledInputView = binding.conversationDisabledInput
|
||||
|
||||
var inputDisabled = true
|
||||
when {
|
||||
inputReadyState.isClientExpired || inputReadyState.isUnauthorized -> disabledInputView.showAsExpiredOrUnauthorized(inputReadyState.isClientExpired, inputReadyState.isUnauthorized)
|
||||
inputReadyState.messageRequestState != MessageRequestState.NONE -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState)
|
||||
inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember()
|
||||
inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember()
|
||||
inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false -> disabledInputView.showAsAnnouncementGroupAdminsOnly()
|
||||
else -> inputDisabled = false
|
||||
}
|
||||
|
||||
inputPanel.setHideForMessageRequestState(inputDisabled)
|
||||
|
||||
if (inputDisabled) {
|
||||
WindowUtil.setNavigationBarColor(requireActivity(), disabledInputView.color)
|
||||
} else {
|
||||
disabledInputView.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateCharactersRemaining() {
|
||||
val messageBody: String = binding.conversationInputPanel.embeddedTextEditor.textTrimmed.toString()
|
||||
val charactersLeftView: TextView = binding.conversationInputSpaceLeft
|
||||
@@ -473,10 +525,13 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
binding.conversationWallpaperDim.visible = false
|
||||
}
|
||||
|
||||
binding.conversationWallpaper.visible = chatWallpaper != null
|
||||
binding.scrollToBottom.setWallpaperEnabled(chatWallpaper != null)
|
||||
binding.scrollToMention.setWallpaperEnabled(chatWallpaper != null)
|
||||
adapter.onHasWallpaperChanged(chatWallpaper != null)
|
||||
val wallpaperEnabled = chatWallpaper != null
|
||||
binding.conversationWallpaper.visible = wallpaperEnabled
|
||||
binding.scrollToBottom.setWallpaperEnabled(wallpaperEnabled)
|
||||
binding.scrollToMention.setWallpaperEnabled(wallpaperEnabled)
|
||||
binding.conversationDisabledInput.setWallpaperEnabled(wallpaperEnabled)
|
||||
|
||||
adapter.onHasWallpaperChanged(wallpaperEnabled)
|
||||
}
|
||||
|
||||
private fun presentChatColors(chatColors: ChatColors) {
|
||||
@@ -1238,6 +1293,128 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
}
|
||||
}
|
||||
|
||||
//region Disabled Input Callbacks
|
||||
|
||||
private inner class DisabledInputListener : DisabledInputView.Listener {
|
||||
override fun onUpdateAppClicked() {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
|
||||
}
|
||||
|
||||
override fun onReRegisterClicked() {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
}
|
||||
|
||||
override fun onCancelGroupRequestClicked() {
|
||||
conversationGroupViewModel
|
||||
.cancelJoinRequest()
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> Log.d(TAG, "Cancel request complete")
|
||||
is Result.Failure -> {
|
||||
Log.d(TAG, "Cancel join request failed ${result.failure}")
|
||||
toast(GroupErrors.getUserDisplayMessage(result.failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
override fun onShowAdminsBottomSheetDialog() {
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
ShowAdminsBottomSheetDialog.show(childFragmentManager, recipient.requireGroupId().requireV2())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAcceptMessageRequestClicked() {
|
||||
messageRequestViewModel
|
||||
.onAccept()
|
||||
.subscribeWithShowProgress("accept message request")
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
override fun onDeleteGroupClicked() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onDeleteGroupClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
ConversationDialogs.displayDeleteDialog(requireContext(), recipient) {
|
||||
messageRequestViewModel
|
||||
.onDelete()
|
||||
.subscribeWithShowProgress("delete message request")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBlockClicked() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onBlockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showBlockAndReportSpamFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient,
|
||||
{
|
||||
messageRequestViewModel
|
||||
.onBlock()
|
||||
.subscribeWithShowProgress("block")
|
||||
},
|
||||
{
|
||||
messageRequestViewModel
|
||||
.onBlockAndReportSpam()
|
||||
.subscribeWithShowProgress("block")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onUnblockClicked() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onUnblockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showUnblockFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient
|
||||
) {
|
||||
messageRequestViewModel
|
||||
.onUnblock()
|
||||
.subscribeWithShowProgress("unblock")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGroupV1MigrationClicked() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onGroupV1MigrationClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(childFragmentManager, recipient.id)
|
||||
}
|
||||
|
||||
private fun Single<Result<Unit, GroupChangeFailureReason>>.subscribeWithShowProgress(logMessage: String): Disposable {
|
||||
return doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> Log.d(TAG, "$logMessage complete")
|
||||
is Result.Failure -> {
|
||||
Log.d(TAG, "$logMessage failed ${result.failure}")
|
||||
toast(GroupErrors.getUserDisplayMessage(result.failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Compose + Send Callbacks
|
||||
|
||||
private inner class SendButtonListener : View.OnClickListener, OnEditorActionListener {
|
||||
|
||||
@@ -4,7 +4,9 @@ import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Optional
|
||||
|
||||
class ConversationRecipientRepository(threadId: Long) {
|
||||
|
||||
@@ -17,7 +19,22 @@ class ConversationRecipientRepository(threadId: Long) {
|
||||
.flatMapObservable { Recipient.observable(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.distinctUntilChanged { previous, next -> previous === next || previous.hasSameContent(next) }
|
||||
.replay(1)
|
||||
.refCount()
|
||||
.observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
val groupRecord: Observable<Optional<GroupRecord>> by lazy {
|
||||
conversationRecipient
|
||||
.switchMapSingle {
|
||||
Single.fromCallable {
|
||||
if (it.isGroup) {
|
||||
SignalDatabase.groups.getGroup(it.id)
|
||||
} else {
|
||||
Optional.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
.replay(1)
|
||||
.refCount()
|
||||
.observeOn(Schedulers.io())
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
@@ -19,8 +18,8 @@ import io.reactivex.rxjava3.processors.PublishProcessor
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.paging.ProxyPagingController
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents.Args
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
@@ -31,10 +30,13 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Quote
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.hasGiftBadge
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
@@ -46,7 +48,8 @@ class ConversationViewModel(
|
||||
private val threadId: Long,
|
||||
requestedStartingPosition: Int,
|
||||
private val repository: ConversationRepository,
|
||||
recipientRepository: ConversationRecipientRepository
|
||||
recipientRepository: ConversationRecipientRepository,
|
||||
messageRequestRepository: MessageRequestRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
@@ -78,6 +81,8 @@ class ConversationViewModel(
|
||||
val wallpaperSnapshot: ChatWallpaper?
|
||||
get() = _recipient.value?.wallpaper
|
||||
|
||||
val inputReadyState: Observable<InputReadyState>
|
||||
|
||||
init {
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
@@ -129,6 +134,19 @@ class ConversationViewModel(
|
||||
hasMentions = counts.mentions != 0
|
||||
)
|
||||
}
|
||||
|
||||
inputReadyState = Observable.combineLatest(
|
||||
recipientRepository.conversationRecipient,
|
||||
recipientRepository.groupRecord
|
||||
) { recipient, groupRecord ->
|
||||
InputReadyState(
|
||||
conversationRecipient = recipient,
|
||||
messageRequestState = messageRequestRepository.getMessageRequestState(recipient, threadId),
|
||||
groupRecord = groupRecord.orNull(),
|
||||
isClientExpired = SignalStore.misc().isClientDeprecated,
|
||||
isUnauthorized = TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication())
|
||||
)
|
||||
}.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
@@ -188,21 +206,4 @@ class ConversationViewModel(
|
||||
bodyRanges = bodyRanges
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val args: Args,
|
||||
private val repository: ConversationRepository,
|
||||
private val recipientRepository: ConversationRecipientRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(
|
||||
ConversationViewModel(
|
||||
args.threadId,
|
||||
args.startingPosition,
|
||||
repository,
|
||||
recipientRepository
|
||||
)
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
||||
/**
|
||||
* A one-stop-view for all your conversation input disabled needs.
|
||||
*
|
||||
* - Expired client
|
||||
* - No longer registered
|
||||
* - Message Request/Blocked
|
||||
* - Requesting group member
|
||||
* - No longer group member
|
||||
*/
|
||||
class DisabledInputView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val inflater: LayoutInflater by lazy { LayoutInflater.from(context) }
|
||||
|
||||
private var expiredOrUnauthorized: View? = null
|
||||
private var messageRequestView: MessageRequestsBottomView? = null
|
||||
private var noLongerAMember: View? = null
|
||||
private var requestingGroup: View? = null
|
||||
private var announcementGroupOnly: TextView? = null
|
||||
|
||||
private var currentView: View? = null
|
||||
|
||||
var color: Int = 0
|
||||
private set
|
||||
var listener: Listener? = null
|
||||
|
||||
fun showAsExpiredOrUnauthorized(clientExpired: Boolean, unauthorized: Boolean) {
|
||||
expiredOrUnauthorized = show(
|
||||
existingView = expiredOrUnauthorized,
|
||||
create = { inflater.inflate(R.layout.conversation_activity_logged_out_stub, this, false) },
|
||||
bind = {
|
||||
val message = findViewById<TextView>(R.id.logged_out_message)
|
||||
val actionButton = findViewById<MaterialButton>(R.id.logged_out_button)
|
||||
|
||||
message.setText(if (clientExpired) R.string.ExpiredBuildReminder_this_version_of_signal_has_expired else R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device)
|
||||
actionButton.setText(if (clientExpired) R.string.ConversationFragment__update_build else R.string.ConversationFragment__reregister_signal)
|
||||
actionButton.setOnClickListener {
|
||||
if (clientExpired) {
|
||||
listener?.onUpdateAppClicked()
|
||||
} else {
|
||||
listener?.onReRegisterClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun showAsMessageRequest(recipient: Recipient, messageRequestState: MessageRequestState) {
|
||||
messageRequestView = show(
|
||||
existingView = messageRequestView,
|
||||
create = { MessageRequestsBottomView(context) },
|
||||
bind = {
|
||||
setMessageData(MessageRequestViewModel.MessageData(recipient, messageRequestState))
|
||||
setWallpaperEnabled(recipient.hasWallpaper())
|
||||
|
||||
setAcceptOnClickListener { listener?.onAcceptMessageRequestClicked() }
|
||||
setDeleteOnClickListener { listener?.onDeleteGroupClicked() }
|
||||
setBlockOnClickListener { listener?.onBlockClicked() }
|
||||
setUnblockOnClickListener { listener?.onUnblockClicked() }
|
||||
setGroupV1MigrationContinueListener { listener?.onGroupV1MigrationClicked() }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun showAsNoLongerAMember() {
|
||||
noLongerAMember = show(
|
||||
existingView = noLongerAMember,
|
||||
create = { inflater.inflate(R.layout.conversation_no_longer_a_member, this, false) }
|
||||
)
|
||||
}
|
||||
|
||||
fun showAsRequestingMember() {
|
||||
requestingGroup = show(
|
||||
existingView = requestingGroup,
|
||||
create = { inflater.inflate(R.layout.conversation_requesting_bottom_banner, this, false) },
|
||||
bind = {
|
||||
findViewById<View>(R.id.conversation_cancel_request).setOnClickListener {
|
||||
listener?.onCancelGroupRequestClicked()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun showAsAnnouncementGroupAdminsOnly() {
|
||||
announcementGroupOnly = show(
|
||||
existingView = announcementGroupOnly,
|
||||
create = { inflater.inflate(R.layout.conversation_cannot_send_announcement_group, this, false) as TextView },
|
||||
bind = {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
text = SpanUtil.clickSubstring(
|
||||
context,
|
||||
R.string.ConversationActivity_only_s_can_send_messages,
|
||||
R.string.ConversationActivity_admins
|
||||
) {
|
||||
listener?.onShowAdminsBottomSheetDialog()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setWallpaperEnabled(wallpaperEnabled: Boolean) {
|
||||
color = ContextCompat.getColor(context, if (wallpaperEnabled) R.color.wallpaper_bubble_color else R.color.signal_colorBackground)
|
||||
setBackgroundColor(color)
|
||||
}
|
||||
|
||||
fun showBusy() {
|
||||
if (currentView == messageRequestView) {
|
||||
messageRequestView?.showBusy()
|
||||
}
|
||||
}
|
||||
|
||||
fun hideBusy() {
|
||||
if (currentView == messageRequestView) {
|
||||
messageRequestView?.hideBusy()
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
removeAllViews()
|
||||
currentView = null
|
||||
expiredOrUnauthorized = null
|
||||
messageRequestView?.hideBusy()
|
||||
messageRequestView = null
|
||||
noLongerAMember = null
|
||||
requestingGroup = null
|
||||
announcementGroupOnly = null
|
||||
}
|
||||
|
||||
private fun <VIEW : View> show(existingView: VIEW?, create: () -> VIEW, bind: VIEW.() -> Unit = {}): VIEW {
|
||||
if (existingView != currentView) {
|
||||
removeIfNotNull(currentView)
|
||||
}
|
||||
|
||||
val view: VIEW = if (existingView != null) {
|
||||
existingView
|
||||
} else {
|
||||
val newView: VIEW = create()
|
||||
addView(newView, defaultLayoutParams())
|
||||
newView
|
||||
}
|
||||
|
||||
view.bind()
|
||||
|
||||
currentView = view
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun defaultLayoutParams(): LayoutParams {
|
||||
return LayoutParams(LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
private fun removeIfNotNull(view: View?) {
|
||||
if (view != null) {
|
||||
removeView(view)
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onUpdateAppClicked()
|
||||
fun onReRegisterClicked()
|
||||
fun onCancelGroupRequestClicked()
|
||||
fun onShowAdminsBottomSheetDialog()
|
||||
fun onAcceptMessageRequestClicked()
|
||||
fun onDeleteGroupClicked()
|
||||
fun onBlockClicked()
|
||||
fun onUnblockClicked()
|
||||
fun onGroupV1MigrationClicked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* Information necessary for rendering compose input.
|
||||
*/
|
||||
data class InputReadyState(
|
||||
val conversationRecipient: Recipient,
|
||||
val messageRequestState: MessageRequestState,
|
||||
private val groupRecord: GroupRecord?,
|
||||
val isClientExpired: Boolean,
|
||||
val isUnauthorized: Boolean
|
||||
) {
|
||||
private val selfMemberLevel: GroupTable.MemberLevel? = groupRecord?.memberLevel(Recipient.self())
|
||||
|
||||
val isSignalConversation: Boolean = conversationRecipient.registered == RecipientTable.RegisteredState.REGISTERED && Recipient.self().isRegistered
|
||||
val isAnnouncementGroup: Boolean? = groupRecord?.isAnnouncementGroup
|
||||
val isActiveGroup: Boolean? = if (selfMemberLevel == null) null else selfMemberLevel != GroupTable.MemberLevel.NOT_A_MEMBER
|
||||
val isAdmin: Boolean? = selfMemberLevel?.equals(GroupTable.MemberLevel.ADMINISTRATOR)
|
||||
val isRequestingMember: Boolean? = selfMemberLevel?.equals(GroupTable.MemberLevel.REQUESTING_MEMBER)
|
||||
}
|
||||
@@ -1,27 +1,15 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.concurrent.subscribeWithSubject
|
||||
import org.signal.core.util.Result
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.messagerequests.GroupInfo
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.MessageData
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.RequestReviewDisplayState
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel.Status
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* MessageRequestViewModel for ConversationFragment V2
|
||||
* View model for interacting with a message request displayed in ConversationFragment V2
|
||||
*/
|
||||
class MessageRequestViewModel(
|
||||
private val threadId: Long,
|
||||
@@ -29,152 +17,51 @@ class MessageRequestViewModel(
|
||||
private val messageRequestRepository: MessageRequestRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
private val statusSubject = PublishSubject.create<Status>()
|
||||
val status: Observable<Status> = statusSubject
|
||||
|
||||
private val failureSubject = PublishSubject.create<GroupChangeFailureReason>()
|
||||
val failure: Observable<GroupChangeFailureReason> = failureSubject
|
||||
|
||||
private val groupInfo: Observable<GroupInfo> = recipientRepository
|
||||
.conversationRecipient
|
||||
.flatMap { recipient ->
|
||||
Single.create { emitter ->
|
||||
messageRequestRepository.getGroupInfo(recipient.id, emitter::onSuccess)
|
||||
}.toObservable()
|
||||
private val recipientId: Single<RecipientId>
|
||||
get() {
|
||||
return recipientRepository
|
||||
.conversationRecipient
|
||||
.map { it.id }
|
||||
.firstOrError()
|
||||
}
|
||||
|
||||
private val groups: Observable<List<String>> = recipientRepository
|
||||
.conversationRecipient
|
||||
.flatMap { recipient ->
|
||||
Single.create<List<String>> { emitter ->
|
||||
messageRequestRepository.getGroups(recipient.id, emitter::onSuccess)
|
||||
}.toObservable()
|
||||
}
|
||||
|
||||
private val messageDataSubject: BehaviorSubject<MessageData> = recipientRepository.conversationRecipient.map {
|
||||
val state = messageRequestRepository.getMessageRequestState(it, threadId)
|
||||
MessageData(it, state)
|
||||
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
||||
|
||||
private val requestReviewDisplayStateSubject: BehaviorSubject<RequestReviewDisplayState> = messageDataSubject.map { holder ->
|
||||
if (holder.messageState == MessageRequestState.INDIVIDUAL) {
|
||||
if (ReviewUtil.isRecipientReviewSuggested(holder.recipient.id)) {
|
||||
RequestReviewDisplayState.SHOWN
|
||||
} else {
|
||||
RequestReviewDisplayState.HIDDEN
|
||||
fun onAccept(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
messageRequestRepository.acceptMessageRequest(recipientId, threadId)
|
||||
}
|
||||
} else {
|
||||
RequestReviewDisplayState.NONE
|
||||
}
|
||||
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
||||
|
||||
val recipientInfo: Observable<MessageRequestRecipientInfo> = Observable.combineLatest(
|
||||
recipientRepository.conversationRecipient,
|
||||
groupInfo,
|
||||
groups,
|
||||
messageDataSubject.map { it.messageState },
|
||||
::MessageRequestRecipientInfo
|
||||
)
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun shouldShowMessageRequest(): Boolean {
|
||||
val messageData = messageDataSubject.value
|
||||
return messageData != null && messageData.messageState != MessageRequestState.NONE
|
||||
}
|
||||
|
||||
fun onAccept() {
|
||||
statusSubject.onNext(Status.ACCEPTING)
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.firstOrError()
|
||||
.map { it.id }
|
||||
.subscribeBy { recipientId ->
|
||||
messageRequestRepository.acceptMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
{ statusSubject.onNext(Status.ACCEPTED) },
|
||||
this::onGroupChangeError
|
||||
)
|
||||
}
|
||||
}
|
||||
fun onDelete() {
|
||||
statusSubject.onNext(Status.DELETING)
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.firstOrError()
|
||||
.map { it.id }
|
||||
.subscribeBy { recipientId ->
|
||||
messageRequestRepository.deleteMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
{ statusSubject.onNext(Status.DELETED) },
|
||||
this::onGroupChangeError
|
||||
)
|
||||
}
|
||||
}
|
||||
fun onBlock() {
|
||||
statusSubject.onNext(Status.BLOCKING)
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.firstOrError()
|
||||
.map { it.id }
|
||||
.subscribeBy { recipientId ->
|
||||
messageRequestRepository.blockMessageRequest(
|
||||
recipientId,
|
||||
{ statusSubject.onNext(Status.BLOCKED) },
|
||||
this::onGroupChangeError
|
||||
)
|
||||
}
|
||||
}
|
||||
fun onUnblock() {
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.firstOrError()
|
||||
.map { it.id }
|
||||
.subscribeBy { recipientId ->
|
||||
messageRequestRepository.unblockAndAccept(
|
||||
recipientId
|
||||
) { statusSubject.onNext(Status.ACCEPTED) }
|
||||
}
|
||||
}
|
||||
fun onBlockAndReportSpam() {
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.firstOrError()
|
||||
.map { it.id }
|
||||
.subscribeBy { recipientId ->
|
||||
messageRequestRepository.blockAndReportSpamMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
{ statusSubject.onNext(Status.BLOCKED_AND_REPORTED) },
|
||||
this::onGroupChangeError
|
||||
)
|
||||
fun onDelete(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
messageRequestRepository.deleteMessageRequest(recipientId, threadId)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun onGroupChangeError(error: GroupChangeFailureReason) {
|
||||
statusSubject.onNext(Status.IDLE)
|
||||
failureSubject.onNext(error)
|
||||
fun onBlock(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
messageRequestRepository.blockMessageRequest(recipientId)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val threadId: Long,
|
||||
private val recipientRepository: ConversationRecipientRepository,
|
||||
private val messageRequestRepository: MessageRequestRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(
|
||||
MessageRequestViewModel(
|
||||
threadId,
|
||||
recipientRepository,
|
||||
messageRequestRepository
|
||||
)
|
||||
) as T
|
||||
}
|
||||
fun onUnblock(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
messageRequestRepository.unblockAndAccept(recipientId)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun onBlockAndReportSpam(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
messageRequestRepository.blockAndReportSpamMessageRequest(recipientId, threadId)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.concurrent.subscribeWithSubject
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationRecipientRepository
|
||||
import org.thoughtcrime.securesms.database.GroupTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil
|
||||
@@ -31,43 +32,36 @@ class ConversationGroupViewModel(
|
||||
) : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _recipient: Subject<Recipient> = BehaviorSubject.create()
|
||||
private val _groupRecord: Subject<GroupRecord> = BehaviorSubject.create()
|
||||
private val _groupRecord: Subject<GroupRecord>
|
||||
private val _reviewState: Subject<ConversationGroupReviewState>
|
||||
|
||||
private val _groupActiveState: Subject<ConversationGroupActiveState> = BehaviorSubject.create()
|
||||
private val _memberLevel: BehaviorSubject<ConversationGroupMemberLevel> = BehaviorSubject.create()
|
||||
private val _actionableRequestingMembersCount: Subject<Int> = BehaviorSubject.create()
|
||||
private val _gv1MigrationSuggestions: Subject<List<RecipientId>> = BehaviorSubject.create()
|
||||
private val _reviewState: Subject<ConversationGroupReviewState> = BehaviorSubject.create()
|
||||
|
||||
init {
|
||||
disposables += recipientRepository
|
||||
.conversationRecipient
|
||||
.filter { it.isGroup }
|
||||
.subscribeBy(onNext = _recipient::onNext)
|
||||
_groupRecord = recipientRepository
|
||||
.groupRecord
|
||||
.filter { it.isPresent }
|
||||
.map { it.get() }
|
||||
.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
||||
|
||||
disposables += _recipient
|
||||
.switchMap {
|
||||
Observable.fromCallable {
|
||||
SignalDatabase.groups.getGroup(it.id).get()
|
||||
}
|
||||
}
|
||||
.subscribeBy(onNext = _groupRecord::onNext)
|
||||
|
||||
val duplicates = _groupRecord.map {
|
||||
if (it.isV2Group) {
|
||||
ReviewUtil.getDuplicatedRecipients(it.id.requireV2()).map { it.recipient }
|
||||
val duplicates = _groupRecord.map { groupRecord ->
|
||||
if (groupRecord.isV2Group) {
|
||||
ReviewUtil.getDuplicatedRecipients(groupRecord.id.requireV2()).map { it.recipient }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Observable.combineLatest(_groupRecord, duplicates) { record, dupes ->
|
||||
_reviewState = Observable.combineLatest(_groupRecord, duplicates) { record, dupes ->
|
||||
if (dupes.isEmpty()) {
|
||||
ConversationGroupReviewState.EMPTY
|
||||
} else {
|
||||
ConversationGroupReviewState(record.id.requireV2(), dupes[0], dupes.size)
|
||||
}
|
||||
}.subscribeBy(onNext = _reviewState::onNext)
|
||||
}.subscribeWithSubject(BehaviorSubject.create(), disposables)
|
||||
|
||||
disposables += _groupRecord.subscribe { groupRecord ->
|
||||
_groupActiveState.onNext(ConversationGroupActiveState(groupRecord.isActive, groupRecord.isV2Group))
|
||||
@@ -87,9 +81,12 @@ class ConversationGroupViewModel(
|
||||
}
|
||||
|
||||
fun blockJoinRequests(recipient: Recipient): Single<GroupBlockJoinRequestResult> {
|
||||
return _recipient.firstOrError().flatMap {
|
||||
groupManagementRepository.blockJoinRequests(it.requireGroupId().requireV2(), recipient)
|
||||
}.observeOn(AndroidSchedulers.mainThread())
|
||||
return _groupRecord
|
||||
.firstOrError()
|
||||
.flatMap {
|
||||
groupManagementRepository.blockJoinRequests(it.id.requireV2(), recipient)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
private fun getActionableRequestingMembersCount(groupRecord: GroupRecord): Int {
|
||||
@@ -114,6 +111,15 @@ class ConversationGroupViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelJoinRequest(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return _groupRecord
|
||||
.firstOrError()
|
||||
.flatMap { group ->
|
||||
groupManagementRepository.cancelJoinRequest(group.id.requireV2())
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
class Factory(private val threadId: Long, private val recipientRepository: ConversationRecipientRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(ConversationGroupViewModel(threadId, recipientRepository = recipientRepository)) as T
|
||||
|
||||
@@ -4,11 +4,14 @@ import android.content.Context
|
||||
import androidx.core.util.Consumer
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
@@ -77,4 +80,22 @@ class GroupManagementRepository @JvmOverloads constructor(private val context: C
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun cancelJoinRequest(groupId: GroupId.V2): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return Single.create { emitter ->
|
||||
try {
|
||||
GroupManager.cancelJoinRequest(context, groupId)
|
||||
emitter.onSuccess(Result.success(Unit))
|
||||
} catch (gcfe: GroupChangeFailedException) {
|
||||
Log.i(TAG, "Unable to cancel request", gcfe)
|
||||
emitter.onSuccess(Result.failure(GroupChangeFailureReason.fromException(gcfe)))
|
||||
} catch (ioe: IOException) {
|
||||
Log.i(TAG, "Unable to cancel request", ioe)
|
||||
emitter.onSuccess(Result.failure(GroupChangeFailureReason.fromException(ioe)))
|
||||
} catch (gcbe: GroupChangeBusyException) {
|
||||
Log.i(TAG, "Unable to cancel request", gcbe)
|
||||
emitter.onSuccess(Result.failure(GroupChangeFailureReason.fromException(gcbe)))
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.signal.core.util.Result;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
@@ -24,7 +25,6 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReportSpamJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
@@ -38,6 +38,11 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
public final class MessageRequestRepository {
|
||||
|
||||
private static final String TAG = Log.tag(MessageRequestRepository.class);
|
||||
@@ -149,6 +154,19 @@ public final class MessageRequestRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> acceptMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Single.<Result<Unit, GroupChangeFailureReason>>create(emitter -> {
|
||||
acceptMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
() -> emitter.onSuccess(Result.success(Unit.INSTANCE)),
|
||||
reason -> emitter.onSuccess(Result.failure(reason))
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void acceptMessageRequest(@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
@NonNull Runnable onMessageRequestAccepted,
|
||||
@@ -192,6 +210,19 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> deleteMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Single.<Result<Unit, GroupChangeFailureReason>>create(emitter -> {
|
||||
deleteMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
() -> emitter.onSuccess(Result.success(Unit.INSTANCE)),
|
||||
reason -> emitter.onSuccess(Result.failure(reason))
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void deleteMessageRequest(@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
@NonNull Runnable onMessageRequestDeleted,
|
||||
@@ -229,6 +260,18 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> blockMessageRequest(@NonNull RecipientId recipientId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Single.<Result<Unit, GroupChangeFailureReason>>create(emitter -> {
|
||||
blockMessageRequest(
|
||||
recipientId,
|
||||
() -> emitter.onSuccess(Result.success(Unit.INSTANCE)),
|
||||
reason -> emitter.onSuccess(Result.failure(reason))
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void blockMessageRequest(@NonNull RecipientId recipientId,
|
||||
@NonNull Runnable onMessageRequestBlocked,
|
||||
@NonNull GroupChangeErrorCallback error)
|
||||
@@ -252,6 +295,19 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> blockAndReportSpamMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Single.<Result<Unit, GroupChangeFailureReason>>create(emitter -> {
|
||||
blockAndReportSpamMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
() -> emitter.onSuccess(Result.success(Unit.INSTANCE)),
|
||||
reason -> emitter.onSuccess(Result.failure(reason))
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void blockAndReportSpamMessageRequest(@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
@NonNull Runnable onMessageRequestBlocked,
|
||||
@@ -278,6 +334,17 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> unblockAndAccept(@NonNull RecipientId recipientId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Single.<Result<Unit, GroupChangeFailureReason>>create(emitter -> {
|
||||
unblockAndAccept(
|
||||
recipientId,
|
||||
() -> emitter.onSuccess(Result.success(Unit.INSTANCE))
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public void unblockAndAccept(@NonNull RecipientId recipientId, @NonNull Runnable onMessageRequestUnblocked) {
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
@@ -43,6 +43,7 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
|
||||
public MessageRequestsBottomView(Context context) {
|
||||
super(context);
|
||||
onFinishInflate();
|
||||
}
|
||||
|
||||
public MessageRequestsBottomView(Context context, AttributeSet attrs) {
|
||||
|
||||
@@ -88,7 +88,7 @@ public final class LiveRecipient {
|
||||
* @return An rx-flavored {@link Observable}.
|
||||
*/
|
||||
public @NonNull Observable<Recipient> observable() {
|
||||
return subject.distinctUntilChanged(Recipient::hasSameContent);
|
||||
return subject;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -155,17 +155,7 @@ public class Recipient {
|
||||
@AnyThread
|
||||
public static @NonNull Observable<Recipient> observable(@NonNull RecipientId id) {
|
||||
Preconditions.checkNotNull(id, "ID cannot be null");
|
||||
return Observable.<Recipient>create(emitter -> {
|
||||
LiveRecipient live = live(id);
|
||||
emitter.onNext(live.resolve());
|
||||
|
||||
RecipientForeverObserver observer = emitter::onNext;
|
||||
|
||||
live.observeForever(observer);
|
||||
emitter.setCancellable(() -> {
|
||||
live.removeForeverObserver(observer);
|
||||
});
|
||||
}).subscribeOn(Schedulers.io());
|
||||
return live(id).observable().subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1355,6 +1345,7 @@ public class Recipient {
|
||||
Objects.equals(extras, other.extras) &&
|
||||
hasGroupsInCommon == other.hasGroupsInCommon &&
|
||||
Objects.equals(badges, other.badges) &&
|
||||
isActiveGroup == other.isActiveGroup &&
|
||||
Objects.equals(callLinkRoomId, other.callLinkRoomId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
||||
@@ -23,3 +26,12 @@ class ViewModelFactory<MODEL : ViewModel>(private val create: () -> MODEL) : Vie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Fragment.viewModel(
|
||||
noinline create: () -> VM
|
||||
): Lazy<VM> {
|
||||
return viewModels(
|
||||
factoryProducer = ViewModelFactory.factoryProducer(create)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
android:background="@color/signal_transparent_80"
|
||||
android:gravity="center"
|
||||
android:padding="17dp"
|
||||
android:visibility="gone"
|
||||
style="@style/Signal.Text.Preview"
|
||||
tools:text="Only admins can send messages."
|
||||
android:textColor="@color/signal_text_secondary" />
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub" />
|
||||
app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub,conversation_disabled_input " />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/attachment_editor_stub"
|
||||
@@ -150,6 +150,14 @@
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
|
||||
|
||||
<org.thoughtcrime.securesms.conversation.v2.DisabledInputView
|
||||
android:id="@+id/conversation_disabled_input"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@id/keyboard_guideline"
|
||||
app:layout_constraintEnd_toEndOf="@id/parent_end_guideline"
|
||||
app:layout_constraintStart_toStartOf="@id/parent_start_guideline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/conversation_input_space_left"
|
||||
android:layout_width="0dp"
|
||||
|
||||
Reference in New Issue
Block a user