mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Update spam UX and reporting flows.
This commit is contained in:
committed by
Clark Chen
parent
a4fde60c1c
commit
aa76cefb1c
@@ -120,6 +120,12 @@ public class ConversationHeaderView extends ConstraintLayout {
|
||||
return binding.messageRequestDescription;
|
||||
}
|
||||
|
||||
public void setButton(@NonNull CharSequence button, Runnable onClick) {
|
||||
binding.messageRequestButton.setText(button);
|
||||
binding.messageRequestButton.setOnClickListener(v -> onClick.run());
|
||||
binding.messageRequestButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void showBackgroundBubble(boolean enabled) {
|
||||
if (enabled) {
|
||||
setBackgroundResource(R.drawable.wallpaper_bubble_background_18);
|
||||
@@ -146,6 +152,10 @@ public class ConversationHeaderView extends ConstraintLayout {
|
||||
binding.messageRequestDescription.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void hideButton() {
|
||||
binding.messageRequestButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setLinkifyDescription(boolean enable) {
|
||||
binding.messageRequestDescription.setMovementMethod(enable ? LongClickMovementMethod.getInstance(getContext()) : null);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
@@ -53,7 +54,7 @@ internal object ConversationOptionsMenu {
|
||||
hasActiveGroupCall,
|
||||
distributionType,
|
||||
threadId,
|
||||
isInMessageRequest,
|
||||
messageRequestState,
|
||||
isInBubble
|
||||
) = callback.getSnapshot()
|
||||
|
||||
@@ -62,6 +63,23 @@ internal object ConversationOptionsMenu {
|
||||
return
|
||||
}
|
||||
|
||||
if (!messageRequestState.isAccepted) {
|
||||
menuInflater.inflate(R.menu.conversation_message_request, menu)
|
||||
|
||||
if (messageRequestState.isBlocked) {
|
||||
hideMenuItem(menu, R.id.menu_block)
|
||||
hideMenuItem(menu, R.id.menu_accept)
|
||||
} else {
|
||||
hideMenuItem(menu, R.id.menu_unblock)
|
||||
}
|
||||
|
||||
if (messageRequestState.reportedAsSpam) {
|
||||
hideMenuItem(menu, R.id.menu_report_spam)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!afterFirstRenderMode) {
|
||||
createdPreRenderMenu = true
|
||||
if (recipient.isSelf) {
|
||||
@@ -83,12 +101,6 @@ internal object ConversationOptionsMenu {
|
||||
return
|
||||
}
|
||||
|
||||
if (isInMessageRequest && !recipient.isBlocked) {
|
||||
if (isActiveGroup) {
|
||||
menuInflater.inflate(R.menu.conversation_message_requests_group, menu)
|
||||
}
|
||||
}
|
||||
|
||||
if (isPushAvailable) {
|
||||
if (recipient.expiresInSeconds > 0) {
|
||||
if (!isInActiveGroup) {
|
||||
@@ -120,10 +132,6 @@ internal object ConversationOptionsMenu {
|
||||
|
||||
menuInflater.inflate(R.menu.conversation, menu)
|
||||
|
||||
if (isInMessageRequest && !recipient.isBlocked) {
|
||||
hideMenuItem(menu, R.id.menu_conversation_settings)
|
||||
}
|
||||
|
||||
if (!recipient.isGroup && !isPushAvailable && !recipient.isReleaseNotes) {
|
||||
menuInflater.inflate(R.menu.conversation_insecure, menu)
|
||||
}
|
||||
@@ -208,6 +216,11 @@ internal object ConversationOptionsMenu {
|
||||
R.id.menu_expiring_messages_off, R.id.menu_expiring_messages -> callback.handleSelectMessageExpiration()
|
||||
R.id.menu_create_bubble -> callback.handleCreateBubble()
|
||||
R.id.home -> callback.handleGoHome()
|
||||
R.id.menu_block -> callback.handleBlock()
|
||||
R.id.menu_unblock -> callback.handleUnblock()
|
||||
R.id.menu_report_spam -> callback.handleReportSpam()
|
||||
R.id.menu_accept -> callback.handleMessageRequestAccept()
|
||||
R.id.menu_delete_chat -> callback.handleDeleteConversation()
|
||||
R.id.edittext_bold,
|
||||
R.id.edittext_italic,
|
||||
R.id.edittext_strikethrough,
|
||||
@@ -244,7 +257,7 @@ internal object ConversationOptionsMenu {
|
||||
val hasActiveGroupCall: Boolean,
|
||||
val distributionType: Int,
|
||||
val threadId: Long,
|
||||
val isInMessageRequest: Boolean,
|
||||
val messageRequestState: MessageRequestState,
|
||||
val isInBubble: Boolean
|
||||
)
|
||||
|
||||
@@ -276,5 +289,10 @@ internal object ConversationOptionsMenu {
|
||||
fun showExpiring(recipient: Recipient)
|
||||
fun clearExpiring()
|
||||
fun handleFormatText(@IdRes id: Int)
|
||||
fun handleBlock()
|
||||
fun handleUnblock()
|
||||
fun handleReportSpam()
|
||||
fun handleMessageRequestAccept()
|
||||
fun handleDeleteConversation()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +566,22 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
eventListener.onSendPaymentClicked(conversationMessage.getMessageRecord().getFromRecipient().getId());
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isReportedSpam()) {
|
||||
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onReportSpamLearnMoreClicked();
|
||||
}
|
||||
});
|
||||
} else if (conversationMessage.getMessageRecord().isMessageRequestAccepted()) {
|
||||
actionButton.setText(R.string.ConversationUpdateItem_options);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onMessageRequestAcceptOptionsClicked();
|
||||
}
|
||||
});
|
||||
} else{
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
|
||||
@@ -274,6 +274,9 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -257,6 +257,10 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
getAdapterListener().onEditedIndicatorClicked(messageRecord)
|
||||
}
|
||||
|
||||
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -165,6 +165,9 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -50,7 +50,6 @@ import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyOutgoing
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestState
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
@@ -594,13 +593,25 @@ class ConversationAdapterV2(
|
||||
}
|
||||
}
|
||||
|
||||
if (sharedGroups.isEmpty() || isSelf) {
|
||||
conversationBanner.hideButton()
|
||||
|
||||
if (messageRequestState?.isAccepted == false && sharedGroups.isEmpty() && !isSelf && !recipient.isGroup) {
|
||||
conversationBanner.setDescription(context.getString(R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully), R.drawable.symbol_error_circle_24)
|
||||
conversationBanner.setButton(context.getString(R.string.ConversationFragment_safety_tips)) {
|
||||
clickListener.onShowSafetyTips(false)
|
||||
}
|
||||
} else if (messageRequestState?.isAccepted == false && recipient.isGroup && !groupInfo.hasExistingContacts) {
|
||||
conversationBanner.setDescription(context.getString(R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully), R.drawable.symbol_error_circle_24)
|
||||
conversationBanner.setButton(context.getString(R.string.ConversationFragment_safety_tips)) {
|
||||
clickListener.onShowSafetyTips(true)
|
||||
}
|
||||
} else if (sharedGroups.isEmpty() || isSelf) {
|
||||
if (TextUtils.isEmpty(groupInfo.description)) {
|
||||
conversationBanner.setLinkifyDescription(false)
|
||||
conversationBanner.hideDescription()
|
||||
} else {
|
||||
conversationBanner.setLinkifyDescription(true)
|
||||
val linkifyWebLinks = messageRequestState == MessageRequestState.NONE
|
||||
val linkifyWebLinks = messageRequestState?.isAccepted == true
|
||||
conversationBanner.showDescription()
|
||||
|
||||
GroupDescriptionUtil.setText(
|
||||
|
||||
@@ -10,12 +10,10 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.NoGroupsInCommon
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
|
||||
|
||||
/**
|
||||
@@ -82,24 +80,7 @@ object ConversationDialogs {
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun displayInMemoryMessageDialog(context: Context, messageRecord: MessageRecord) {
|
||||
if (messageRecord is NoGroupsInCommon) {
|
||||
val isGroup = messageRecord.isGroup
|
||||
MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Signal_MaterialAlertDialog)
|
||||
.setMessage(
|
||||
if (isGroup) {
|
||||
R.string.GroupsInCommonMessageRequest__none_of_your_contacts_or_people_you_chat_with_are_in_this_group
|
||||
} else {
|
||||
R.string.GroupsInCommonMessageRequest__you_have_no_groups_in_common_with_this_person
|
||||
}
|
||||
)
|
||||
.setNeutralButton(R.string.GroupsInCommonMessageRequest__about_message_requests) { _, _ ->
|
||||
CommunicationActions.openBrowserLink(context, context.getString(R.string.GroupsInCommonMessageRequest__support_article))
|
||||
}
|
||||
.setPositiveButton(R.string.GroupsInCommonMessageRequest__okay, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
fun displayInMemoryMessageDialog(context: Context, messageRecord: MessageRecord) = Unit
|
||||
|
||||
fun displayMessageCouldNotBeSentDialog(context: Context, messageRecord: MessageRecord) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
|
||||
@@ -243,7 +243,6 @@ import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
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.AudioSlide
|
||||
import org.thoughtcrime.securesms.mms.GifSlide
|
||||
@@ -1141,7 +1140,7 @@ class ConversationFragment :
|
||||
var inputDisabled = true
|
||||
when {
|
||||
inputReadyState.isClientExpired || inputReadyState.isUnauthorized -> disabledInputView.showAsExpiredOrUnauthorized(inputReadyState.isClientExpired, inputReadyState.isUnauthorized)
|
||||
inputReadyState.messageRequestState != MessageRequestState.NONE && inputReadyState.messageRequestState != MessageRequestState.NONE_HIDDEN -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState)
|
||||
!inputReadyState.messageRequestState.isAccepted -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState)
|
||||
inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember()
|
||||
inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember()
|
||||
inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false -> disabledInputView.showAsAnnouncementGroupAdminsOnly()
|
||||
@@ -2099,6 +2098,128 @@ class ConversationFragment :
|
||||
composeText.clearFocus()
|
||||
}
|
||||
|
||||
//region Message Request Helpers
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun onReportSpam() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onBlockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showReportSpamFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient,
|
||||
{
|
||||
messageRequestViewModel
|
||||
.onReportSpam()
|
||||
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
.subscribeBy {
|
||||
Log.d(TAG, "report spam complete")
|
||||
toast(R.string.ConversationFragment_reported_as_spam)
|
||||
}
|
||||
},
|
||||
if (recipient.isBlocked) {
|
||||
null
|
||||
} else {
|
||||
Runnable {
|
||||
messageRequestViewModel
|
||||
.onBlockAndReportSpam()
|
||||
.doOnSubscribe { binding.conversationDisabledInput.showBusy() }
|
||||
.doOnTerminate { binding.conversationDisabledInput.hideBusy() }
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Log.d(TAG, "report spam complete")
|
||||
toast(R.string.ConversationFragment_reported_as_spam_and_blocked)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
Log.d(TAG, "report spam failed ${result.failure}")
|
||||
toast(GroupErrors.getUserDisplayMessage(result.failure))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun onBlock() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onBlockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showBlockFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient
|
||||
) {
|
||||
messageRequestViewModel
|
||||
.onBlock()
|
||||
.subscribeWithShowProgress("block")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun onUnblock() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onUnblockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showUnblockFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient
|
||||
) {
|
||||
messageRequestViewModel
|
||||
.onUnblock()
|
||||
.subscribeWithShowProgress("unblock")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMessageRequestAccept() {
|
||||
messageRequestViewModel
|
||||
.onAccept()
|
||||
.subscribeWithShowProgress("accept message request")
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
private fun onDeleteConversation() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onDeleteConversation] No recipient!")
|
||||
return
|
||||
}
|
||||
|
||||
ConversationDialogs.displayDeleteDialog(requireContext(), recipient) {
|
||||
messageRequestViewModel
|
||||
.onDelete()
|
||||
.subscribeWithShowProgress("delete message request")
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class BackPressedDelegate : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "onBackPressed()")
|
||||
@@ -2115,6 +2236,8 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
//region Message action handling
|
||||
|
||||
private fun handleReplyToMessage(conversationMessage: ConversationMessage) {
|
||||
@@ -2983,6 +3106,31 @@ class ConversationFragment :
|
||||
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey)
|
||||
}
|
||||
|
||||
override fun onShowSafetyTips(forGroup: Boolean) {
|
||||
SafetyTipsBottomSheetDialog.show(childFragmentManager, forGroup)
|
||||
}
|
||||
|
||||
override fun onReportSpamLearnMoreClicked() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ConversationFragment_reported_spam)
|
||||
.setMessage(R.string.ConversationFragment_reported_spam_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onMessageRequestAcceptOptionsClicked() {
|
||||
val recipient: Recipient? = viewModel.recipientSnapshot
|
||||
|
||||
if (recipient != null) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(getString(R.string.ConversationFragment_you_accepted_a_message_request_from_s, recipient.getDisplayName(requireContext())))
|
||||
.setPositiveButton(R.string.ConversationFragment_block) { _, _ -> onBlock() }
|
||||
.setNegativeButton(R.string.ConversationFragment_report_spam) { _, _ -> onReportSpam() }
|
||||
.setNeutralButton(R.string.ConversationFragment__cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageRecord.getAudioUriForLongClick(): Uri? {
|
||||
val playbackState = getVoiceNoteMediaController().voiceNotePlaybackState.value
|
||||
if (playbackState == null || !playbackState.isPlaying) {
|
||||
@@ -3012,7 +3160,7 @@ class ConversationFragment :
|
||||
hasActiveGroupCall = groupCallViewModel.hasOngoingGroupCallSnapshot,
|
||||
distributionType = args.distributionType,
|
||||
threadId = args.threadId,
|
||||
isInMessageRequest = viewModel.hasMessageRequestState,
|
||||
messageRequestState = viewModel.messageRequestState,
|
||||
isInBubble = args.conversationScreenType.isInBubble
|
||||
)
|
||||
}
|
||||
@@ -3240,6 +3388,26 @@ class ConversationFragment :
|
||||
override fun handleFormatText(id: Int) {
|
||||
composeText.handleFormatText(id)
|
||||
}
|
||||
|
||||
override fun handleBlock() {
|
||||
onBlock()
|
||||
}
|
||||
|
||||
override fun handleUnblock() {
|
||||
onUnblock()
|
||||
}
|
||||
|
||||
override fun handleReportSpam() {
|
||||
onReportSpam()
|
||||
}
|
||||
|
||||
override fun handleMessageRequestAccept() {
|
||||
onMessageRequestAccept()
|
||||
}
|
||||
|
||||
override fun handleDeleteConversation() {
|
||||
onDeleteConversation()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OnReactionsSelectedListener : ConversationReactionOverlay.OnReactionSelectedListener {
|
||||
@@ -3551,66 +3719,23 @@ class ConversationFragment :
|
||||
}
|
||||
|
||||
override fun onAcceptMessageRequestClicked() {
|
||||
messageRequestViewModel
|
||||
.onAccept()
|
||||
.subscribeWithShowProgress("accept message request")
|
||||
.addTo(disposables)
|
||||
onMessageRequestAccept()
|
||||
}
|
||||
|
||||
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 onDeleteClicked() {
|
||||
onDeleteConversation()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
)
|
||||
onBlock()
|
||||
}
|
||||
|
||||
override fun onUnblockClicked() {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient == null) {
|
||||
Log.w(TAG, "[onUnblockClicked] No recipient!")
|
||||
return
|
||||
}
|
||||
onUnblock()
|
||||
}
|
||||
|
||||
BlockUnblockDialog.showUnblockFor(
|
||||
requireContext(),
|
||||
lifecycle,
|
||||
recipient
|
||||
) {
|
||||
messageRequestViewModel
|
||||
.onUnblock()
|
||||
.subscribeWithShowProgress("unblock")
|
||||
}
|
||||
override fun onReportSpamClicked() {
|
||||
onReportSpam()
|
||||
}
|
||||
|
||||
override fun onInviteToSignal(recipient: Recipient) {
|
||||
@@ -3625,20 +3750,6 @@ class ConversationFragment :
|
||||
override fun onUnmuteReleaseNotesChannel() {
|
||||
viewModel.muteConversation(0L)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -366,7 +366,7 @@ class ConversationRepository(
|
||||
|
||||
fun getRequestReviewState(recipient: Recipient, group: GroupRecord?, messageRequest: MessageRequestState): Single<RequestReviewState> {
|
||||
return Single.fromCallable {
|
||||
if (group == null && messageRequest != MessageRequestState.INDIVIDUAL) {
|
||||
if (group == null && messageRequest.state != MessageRequestState.State.INDIVIDUAL) {
|
||||
return@fromCallable RequestReviewState()
|
||||
}
|
||||
|
||||
|
||||
@@ -132,9 +132,11 @@ class ConversationViewModel(
|
||||
private val _inputReadyState: Observable<InputReadyState>
|
||||
val inputReadyState: Observable<InputReadyState>
|
||||
|
||||
private val hasMessageRequestStateSubject: BehaviorSubject<Boolean> = BehaviorSubject.createDefault(false)
|
||||
private val hasMessageRequestStateSubject: BehaviorSubject<MessageRequestState> = BehaviorSubject.createDefault(MessageRequestState())
|
||||
val hasMessageRequestState: Boolean
|
||||
get() = hasMessageRequestStateSubject.value ?: false
|
||||
get() = hasMessageRequestStateSubject.value?.state != MessageRequestState.State.NONE
|
||||
val messageRequestState: MessageRequestState
|
||||
get() = hasMessageRequestStateSubject.value ?: MessageRequestState()
|
||||
|
||||
private val refreshReminder: Subject<Unit> = PublishSubject.create()
|
||||
val reminder: Observable<Optional<Reminder>>
|
||||
@@ -239,7 +241,7 @@ class ConversationViewModel(
|
||||
isUnauthorized = TextSecurePreferences.isUnauthorizedReceived(ApplicationDependencies.getApplication())
|
||||
)
|
||||
}.doOnNext {
|
||||
hasMessageRequestStateSubject.onNext(it.messageRequestState != MessageRequestState.NONE)
|
||||
hasMessageRequestStateSubject.onNext(it.messageRequestState)
|
||||
}
|
||||
inputReadyState = _inputReadyState.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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
|
||||
@@ -80,13 +79,14 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
existingView = messageRequestView,
|
||||
create = { MessageRequestsBottomView(context) },
|
||||
bind = {
|
||||
setMessageData(MessageRequestViewModel.MessageData(recipient, messageRequestState))
|
||||
setMessageRequestData(recipient, messageRequestState)
|
||||
setWallpaperEnabled(recipient.hasWallpaper())
|
||||
|
||||
setAcceptOnClickListener { listener?.onAcceptMessageRequestClicked() }
|
||||
setDeleteOnClickListener { listener?.onDeleteGroupClicked() }
|
||||
setDeleteOnClickListener { listener?.onDeleteClicked() }
|
||||
setBlockOnClickListener { listener?.onBlockClicked() }
|
||||
setUnblockOnClickListener { listener?.onUnblockClicked() }
|
||||
setReportOnClickListener { listener?.onReportSpamClicked() }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -226,10 +226,11 @@ class DisabledInputView @JvmOverloads constructor(
|
||||
fun onCancelGroupRequestClicked()
|
||||
fun onShowAdminsBottomSheetDialog()
|
||||
fun onAcceptMessageRequestClicked()
|
||||
fun onDeleteGroupClicked()
|
||||
fun onDeleteClicked()
|
||||
fun onBlockClicked()
|
||||
fun onUnblockClicked()
|
||||
fun onInviteToSignal(recipient: Recipient)
|
||||
fun onUnmuteReleaseNotesChannel()
|
||||
fun onReportSpamClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.Result
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
@@ -57,6 +58,14 @@ class MessageRequestViewModel(
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun onReportSpam(): Completable {
|
||||
return recipientId
|
||||
.flatMapCompletable { recipientId ->
|
||||
messageRequestRepository.reportSpamMessageRequest(recipientId, threadId)
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun onBlockAndReportSpam(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.BottomSheets
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
|
||||
/**
|
||||
* Shows tips about typical spam and fraud messages.
|
||||
*/
|
||||
class SafetyTipsBottomSheetDialog : ComposeBottomSheetDialogFragment() {
|
||||
companion object {
|
||||
private const val FOR_GROUP_ARG = "for_group"
|
||||
|
||||
fun show(fragmentManager: FragmentManager, forGroup: Boolean) {
|
||||
SafetyTipsBottomSheetDialog()
|
||||
.apply {
|
||||
arguments = bundleOf(
|
||||
FOR_GROUP_ARG to forGroup
|
||||
)
|
||||
}
|
||||
.show(fragmentManager, "SAFETY_TIPS")
|
||||
}
|
||||
}
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val nestedScrollInterop = rememberNestedScrollInteropConnection()
|
||||
SafetyTipsContent(
|
||||
forGroup = requireArguments().getBoolean(FOR_GROUP_ARG, false),
|
||||
modifier = Modifier.nestedScroll(nestedScrollInterop)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SafetyTipData(
|
||||
@DrawableRes val heroImage: Int,
|
||||
@StringRes val titleText: Int,
|
||||
@StringRes val messageText: Int
|
||||
)
|
||||
|
||||
private val tips = listOf(
|
||||
SafetyTipData(heroImage = R.drawable.safety_tip1, titleText = R.string.SafetyTips_tip1_title, messageText = R.string.SafetyTips_tip1_message),
|
||||
SafetyTipData(heroImage = R.drawable.safety_tip2, titleText = R.string.SafetyTips_tip2_title, messageText = R.string.SafetyTips_tip2_message),
|
||||
SafetyTipData(heroImage = R.drawable.safety_tip3, titleText = R.string.SafetyTips_tip3_title, messageText = R.string.SafetyTips_tip3_message),
|
||||
SafetyTipData(heroImage = R.drawable.safety_tip4, titleText = R.string.SafetyTips_tip4_title, messageText = R.string.SafetyTips_tip4_message)
|
||||
)
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SafetyTipsContentPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
SafetyTipsContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SafetyTipsContent(forGroup: Boolean = false, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
BottomSheets.Handle()
|
||||
}
|
||||
|
||||
val size = remember { tips.size }
|
||||
val pagerState = rememberPagerState(
|
||||
pageCount = { size }
|
||||
)
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.weight(weight = 1f, fill = false)
|
||||
.padding(top = 22.dp)
|
||||
.verticalScroll(state = scrollState)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.SafetyTips_title),
|
||||
style = MaterialTheme.typography.headlineMedium.copy(textAlign = TextAlign.Center),
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 4.dp, top = 26.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (forGroup) stringResource(id = R.string.SafetyTips_subtitle_group) else stringResource(id = R.string.SafetyTips_subtitle_individual),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
modifier = Modifier
|
||||
.padding(start = 36.dp, end = 36.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
beyondBoundsPageCount = size,
|
||||
modifier = Modifier.padding(top = 24.dp)
|
||||
) {
|
||||
SafetyTip(tips[it])
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 20.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
repeat(pagerState.pageCount) { iteration ->
|
||||
val color = if (pagerState.currentPage == iteration) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(3.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.size(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = SignalTheme.colors.colorSurface1,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 36.dp, top = 24.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage - 1)
|
||||
}
|
||||
},
|
||||
enabled = pagerState.currentPage > 0,
|
||||
modifier = Modifier
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.SafetyTips_previous_tip))
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
},
|
||||
enabled = pagerState.currentPage + 1 < pagerState.pageCount
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.SafetyTips_next_tip))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SafetyTipPreview() {
|
||||
SignalTheme {
|
||||
Surface {
|
||||
SafetyTip(tips[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SafetyTip(safetyTip: SafetyTipData) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = colorResource(id = R.color.safety_tip_background),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, end = 24.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = colorResource(id = R.color.safety_tip_image_background),
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = safetyTip.heroImage),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = safetyTip.titleText),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = safetyTip.messageText),
|
||||
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center),
|
||||
modifier = Modifier
|
||||
.padding(start = 24.dp, end = 24.dp, bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.NoGroupsInCommon
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.RemovedContactHidden
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord.UniversalExpireTimerUpdate
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
@@ -72,7 +71,6 @@ class ConversationDataSource(
|
||||
val startTime = System.currentTimeMillis()
|
||||
val size: Int = getSizeInternal() +
|
||||
THREAD_HEADER_COUNT +
|
||||
messageRequestData.includeWarningUpdateMessage().toInt() +
|
||||
messageRequestData.isHidden.toInt() +
|
||||
showUniversalExpireTimerUpdate.toInt()
|
||||
|
||||
@@ -108,10 +106,6 @@ class ConversationDataSource(
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRequestData.includeWarningUpdateMessage() && (start + length >= totalSize)) {
|
||||
records.add(NoGroupsInCommon(threadId, messageRequestData.isGroup))
|
||||
}
|
||||
|
||||
if (messageRequestData.isHidden && (start + length >= totalSize)) {
|
||||
records.add(RemovedContactHidden(threadId))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user