mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Update spam UX and reporting flows.
This commit is contained in:
committed by
Clark Chen
parent
a4fde60c1c
commit
aa76cefb1c
@@ -123,5 +123,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
|
||||
void onShowSafetyTips(boolean forGroup);
|
||||
void onReportSpamLearnMoreClicked();
|
||||
void onMessageRequestAcceptOptionsClicked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,36 @@ import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* This should be used whenever we want to prompt the user to block/unblock a recipient.
|
||||
*/
|
||||
public final class BlockUnblockDialog {
|
||||
|
||||
private BlockUnblockDialog() { }
|
||||
private BlockUnblockDialog() {}
|
||||
|
||||
public static void showReportSpamFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onReportSpam,
|
||||
@Nullable Runnable onBlockAndReportSpam)
|
||||
{
|
||||
SimpleTask.run(lifecycle,
|
||||
() -> buildReportSpamFor(context, recipient, onReportSpam, onBlockAndReportSpam),
|
||||
AlertDialog.Builder::show);
|
||||
}
|
||||
|
||||
public static void showBlockFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@@ -137,4 +157,37 @@ public final class BlockUnblockDialog {
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static AlertDialog.Builder buildReportSpamFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onReportSpam,
|
||||
@Nullable Runnable onBlockAndReportSpam)
|
||||
{
|
||||
recipient = recipient.resolve();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.BlockUnblockDialog_report_spam_title)
|
||||
.setPositiveButton(R.string.BlockUnblockDialog_report_spam, (d, w) -> onReportSpam.run());
|
||||
|
||||
if (onBlockAndReportSpam != null) {
|
||||
builder.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
|
||||
} else {
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
Recipient adder = SignalDatabase.groups().getGroupInviter(recipient.requireGroupId());
|
||||
if (adder != null) {
|
||||
builder.setMessage(context.getString(R.string.BlockUnblockDialog_report_spam_group_named_adder, adder.getDisplayName(context)));
|
||||
} else {
|
||||
builder.setMessage(R.string.BlockUnblockDialog_report_spam_group_unknown_adder);
|
||||
}
|
||||
} else {
|
||||
builder.setMessage(R.string.BlockUnblockDialog_report_spam_description);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.concurrent.addTo
|
||||
import org.signal.core.util.getParcelableArrayListExtraCompat
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||
@@ -74,6 +76,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentD
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientExporter
|
||||
@@ -112,17 +115,17 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
private val alertDisabledTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary_50) }
|
||||
private val blockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val unblockIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_block_24)
|
||||
}
|
||||
|
||||
private val leaveIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_leave_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
@@ -135,7 +138,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
recipientId = args.recipientId,
|
||||
groupId = ParcelableGroupId.get(groupId),
|
||||
callMessageIds = args.callMessageIds ?: longArrayOf(),
|
||||
repository = ConversationSettingsRepository(requireContext())
|
||||
repository = ConversationSettingsRepository(requireContext()),
|
||||
messageRequestRepository = MessageRequestRepository(requireContext())
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -798,6 +802,49 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val reportSpamTint = if (state.isDeprecatedOrUnregistered) R.color.signal_alert_primary_50 else R.color.signal_alert_primary
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationFragment_report_spam, ContextCompat.getColor(requireContext(), reportSpamTint)),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_spam_24, reportSpamTint),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
BlockUnblockDialog.showReportSpamFor(
|
||||
requireContext(),
|
||||
viewLifecycleOwner.lifecycle,
|
||||
state.recipient,
|
||||
{
|
||||
viewModel
|
||||
.onReportSpam()
|
||||
.subscribeBy {
|
||||
Toast.makeText(requireContext(), R.string.ConversationFragment_reported_as_spam, Toast.LENGTH_SHORT).show()
|
||||
onToolbarNavigationClicked()
|
||||
}
|
||||
.addTo(lifecycleDisposable)
|
||||
},
|
||||
if (state.recipient.isBlocked) {
|
||||
null
|
||||
} else {
|
||||
Runnable {
|
||||
viewModel
|
||||
.onBlockAndReportSpam()
|
||||
.subscribeBy { result ->
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
Toast.makeText(requireContext(), R.string.ConversationFragment_reported_as_spam_and_blocked, Toast.LENGTH_SHORT).show()
|
||||
onToolbarNavigationClicked()
|
||||
}
|
||||
is Result.Failure -> {
|
||||
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(result.failure), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
.addTo(lifecycleDisposable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -8,11 +7,13 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.Result
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.readToList
|
||||
@@ -25,8 +26,10 @@ import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupAddMembersResult
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
@@ -37,6 +40,7 @@ import org.thoughtcrime.securesms.util.livedata.Store
|
||||
sealed class ConversationSettingsViewModel(
|
||||
private val callMessageIds: LongArray,
|
||||
private val repository: ConversationSettingsRepository,
|
||||
private val messageRequestRepository: MessageRequestRepository,
|
||||
specificSettingsState: SpecificSettingsState
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -90,6 +94,27 @@ sealed class ConversationSettingsViewModel(
|
||||
sharedMediaUpdateTrigger.postValue(Unit)
|
||||
}
|
||||
|
||||
fun onReportSpam(): Maybe<Unit> {
|
||||
return if (store.state.threadId > 0 && store.state.recipient != Recipient.UNKNOWN) {
|
||||
messageRequestRepository.reportSpamMessageRequest(store.state.recipient.id, store.state.threadId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.toSingle { Unit }
|
||||
.toMaybe()
|
||||
} else {
|
||||
Maybe.empty()
|
||||
}
|
||||
}
|
||||
|
||||
fun onBlockAndReportSpam(): Maybe<Result<Unit, GroupChangeFailureReason>> {
|
||||
return if (store.state.threadId > 0 && store.state.recipient != Recipient.UNKNOWN) {
|
||||
messageRequestRepository.blockAndReportSpamMessageRequest(store.state.recipient.id, store.state.threadId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.toMaybe()
|
||||
} else {
|
||||
Maybe.empty()
|
||||
}
|
||||
}
|
||||
|
||||
open fun refreshRecipient(): Unit = error("This ViewModel does not support this interaction")
|
||||
|
||||
abstract fun setMuteUntil(muteUntil: Long)
|
||||
@@ -112,19 +137,15 @@ sealed class ConversationSettingsViewModel(
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
private fun Cursor?.ensureClosed() {
|
||||
if (this != null && !this.isClosed) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipientSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val callMessageIds: LongArray,
|
||||
private val repository: ConversationSettingsRepository
|
||||
private val repository: ConversationSettingsRepository,
|
||||
messageRequestRepository: MessageRequestRepository
|
||||
) : ConversationSettingsViewModel(
|
||||
callMessageIds,
|
||||
repository,
|
||||
messageRequestRepository,
|
||||
SpecificSettingsState.RecipientSettingsState()
|
||||
) {
|
||||
|
||||
@@ -252,8 +273,9 @@ sealed class ConversationSettingsViewModel(
|
||||
private class GroupSettingsViewModel(
|
||||
private val groupId: GroupId,
|
||||
private val callMessageIds: LongArray,
|
||||
private val repository: ConversationSettingsRepository
|
||||
) : ConversationSettingsViewModel(callMessageIds, repository, SpecificSettingsState.GroupSettingsState(groupId)) {
|
||||
private val repository: ConversationSettingsRepository,
|
||||
messageRequestRepository: MessageRequestRepository
|
||||
) : ConversationSettingsViewModel(callMessageIds, repository, messageRequestRepository, SpecificSettingsState.GroupSettingsState(groupId)) {
|
||||
|
||||
private val liveGroup = LiveGroup(groupId)
|
||||
|
||||
@@ -465,15 +487,16 @@ sealed class ConversationSettingsViewModel(
|
||||
private val recipientId: RecipientId? = null,
|
||||
private val groupId: GroupId? = null,
|
||||
private val callMessageIds: LongArray,
|
||||
private val repository: ConversationSettingsRepository
|
||||
private val repository: ConversationSettingsRepository,
|
||||
private val messageRequestRepository: MessageRequestRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(
|
||||
modelClass.cast(
|
||||
when {
|
||||
recipientId != null -> RecipientSettingsViewModel(recipientId, callMessageIds, repository)
|
||||
groupId != null -> GroupSettingsViewModel(groupId, callMessageIds, repository)
|
||||
recipientId != null -> RecipientSettingsViewModel(recipientId, callMessageIds, repository, messageRequestRepository)
|
||||
groupId != null -> GroupSettingsViewModel(groupId, callMessageIds, repository, messageRequestRepository)
|
||||
else -> error("One of RecipientId or GroupId required.")
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.Cursor
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.contentValuesOf
|
||||
import okio.ByteString
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.SqlUtil.appendArg
|
||||
@@ -32,6 +33,7 @@ import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchSortOrder
|
||||
import org.thoughtcrime.securesms.contacts.paged.collections.ContactSearchIterator
|
||||
import org.thoughtcrime.securesms.crypto.SenderKeyUtil
|
||||
@@ -57,6 +59,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI.Companion.parseOrNull
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.io.Closeable
|
||||
import java.security.SecureRandom
|
||||
@@ -639,6 +642,27 @@ class GroupTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupInviter(groupId: GroupId): Recipient? {
|
||||
val groupRecord: Optional<GroupRecord> = getGroup(groupId)
|
||||
|
||||
if (groupRecord.isPresent && groupRecord.get().isV2Group) {
|
||||
val pendingMembers: List<DecryptedPendingMember> = groupRecord.get().requireV2GroupProperties().decryptedGroup.pendingMembers
|
||||
val invitedByAci: ByteString? = DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requireAci())
|
||||
.or { DecryptedGroupUtil.findPendingByServiceId(pendingMembers, Recipient.self().requirePni()) }
|
||||
.map { it.addedByAci }
|
||||
.orElse(null)
|
||||
|
||||
if (invitedByAci != null) {
|
||||
val serviceId: ServiceId? = parseOrNull(invitedByAci)
|
||||
if (serviceId != null) {
|
||||
return Recipient.externalPush(serviceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
fun create(groupId: GroupId.V1, title: String?, members: Collection<RecipientId>, avatar: SignalServiceAttachmentPointer?): Boolean {
|
||||
if (groupExists(groupId.deriveV2MigrationGroupId())) {
|
||||
|
||||
@@ -405,6 +405,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$LATEST_REVISION_ID IS NULL AND
|
||||
$TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT} = 0 AND
|
||||
$TYPE & ${MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT} = 0 AND
|
||||
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND
|
||||
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED} AND
|
||||
$TYPE NOT IN (
|
||||
${MessageTypes.PROFILE_CHANGE_TYPE},
|
||||
${MessageTypes.GV1_MIGRATION_TYPE},
|
||||
@@ -1728,7 +1730,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$TYPE != ${MessageTypes.CHANGE_NUMBER_TYPE} AND
|
||||
$TYPE != ${MessageTypes.SMS_EXPORT_TYPE} AND
|
||||
$TYPE != ${MessageTypes.BOOST_REQUEST_TYPE} AND
|
||||
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS}
|
||||
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND
|
||||
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM} AND
|
||||
$TYPE & ${MessageTypes.SPECIAL_TYPES_MASK} != ${MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED}
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -2388,6 +2392,18 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn
|
||||
)
|
||||
} else if (MessageTypes.isReportedSpam(outboxType)) {
|
||||
OutgoingMessage.reportSpamMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn
|
||||
)
|
||||
} else if (MessageTypes.isMessageRequestAccepted(outboxType)) {
|
||||
OutgoingMessage.messageRequestAcceptMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = timestamp,
|
||||
expiresIn = expiresIn
|
||||
)
|
||||
} else {
|
||||
val giftBadge: GiftBadge? = if (body != null && MessageTypes.isGiftBadge(outboxType)) {
|
||||
GiftBadge.ADAPTER.decode(Base64.decode(body))
|
||||
@@ -2552,7 +2568,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
val isNotStoryGroupReply = retrieved.parentStoryId == null || !retrieved.parentStoryId.isGroupReply()
|
||||
|
||||
if (!MessageTypes.isPaymentsActivated(type) && !MessageTypes.isPaymentsRequestToActivate(type) && !MessageTypes.isExpirationTimerUpdate(type) && !retrieved.storyType.isStory && isNotStoryGroupReply && !silent) {
|
||||
if (!MessageTypes.isPaymentsActivated(type) &&
|
||||
!MessageTypes.isPaymentsRequestToActivate(type) &&
|
||||
!MessageTypes.isReportedSpam(type) &&
|
||||
!MessageTypes.isMessageRequestAccepted(type) &&
|
||||
!MessageTypes.isExpirationTimerUpdate(type) &&
|
||||
!retrieved.storyType.isStory &&
|
||||
isNotStoryGroupReply &&
|
||||
!silent
|
||||
) {
|
||||
val incrementUnreadMentions = retrieved.mentions.isNotEmpty() && retrieved.mentions.any { it.recipientId == Recipient.self().id }
|
||||
threads.incrementUnread(threadId, 1, if (incrementUnreadMentions) 1 else 0)
|
||||
ThreadUpdateJob.enqueue(threadId)
|
||||
@@ -2782,6 +2806,22 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
hasSpecialType = true
|
||||
}
|
||||
|
||||
if (message.isReportSpam) {
|
||||
if (hasSpecialType) {
|
||||
throw MmsException("Cannot insert message with multiple special types.")
|
||||
}
|
||||
type = type or MessageTypes.SPECIAL_TYPE_REPORTED_SPAM
|
||||
hasSpecialType = true
|
||||
}
|
||||
|
||||
if (message.isMessageRequestAccept) {
|
||||
if (hasSpecialType) {
|
||||
throw MmsException("Cannot insert message with multiple special types.")
|
||||
}
|
||||
type = type or MessageTypes.SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED
|
||||
hasSpecialType = true
|
||||
}
|
||||
|
||||
val earlyDeliveryReceipts: Map<RecipientId, Receipt> = earlyDeliveryReceiptCache.remove(message.sentTimeMillis)
|
||||
|
||||
if (earlyDeliveryReceipts.isNotEmpty()) {
|
||||
@@ -3533,6 +3573,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
.run()
|
||||
}
|
||||
|
||||
fun hasReportSpamMessage(threadId: Long): Boolean {
|
||||
return readableDatabase
|
||||
.exists(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND ($TYPE & ${MessageTypes.SPECIAL_TYPES_MASK}) = ${MessageTypes.SPECIAL_TYPE_REPORTED_SPAM}")
|
||||
.run()
|
||||
}
|
||||
|
||||
private val outgoingInsecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND NOT ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT})"
|
||||
private val outgoingSecureMessageClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_SENT_TYPE} AND ($TYPE & ${MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT})"
|
||||
|
||||
@@ -4011,6 +4058,33 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
.take(limit)
|
||||
}
|
||||
|
||||
fun getGroupReportSpamMessageServerData(threadId: Long, inviter: RecipientId, timestamp: Long, limit: Int): List<ReportSpamData> {
|
||||
val data: MutableList<ReportSpamData> = ArrayList()
|
||||
|
||||
val incomingGroupUpdateClause = "($TYPE & ${MessageTypes.BASE_TYPE_MASK}) = ${MessageTypes.BASE_INBOX_TYPE} AND ($TYPE & ${MessageTypes.GROUP_UPDATE_BIT}) != 0"
|
||||
|
||||
readableDatabase
|
||||
.select(FROM_RECIPIENT_ID, SERVER_GUID, DATE_RECEIVED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$FROM_RECIPIENT_ID = ? AND $THREAD_ID = ? AND $DATE_RECEIVED <= ? AND $incomingGroupUpdateClause", inviter, threadId, timestamp)
|
||||
.orderBy("$DATE_RECEIVED DESC")
|
||||
.limit(limit)
|
||||
.run()
|
||||
.forEach { cursor ->
|
||||
val serverGuid: String? = cursor.requireString(SERVER_GUID)
|
||||
|
||||
if (serverGuid != null && serverGuid.isNotEmpty()) {
|
||||
data += ReportSpamData(
|
||||
recipientId = RecipientId.from(cursor.requireLong(FROM_RECIPIENT_ID)),
|
||||
serverGuid = serverGuid,
|
||||
dateReceived = cursor.requireLong(DATE_RECEIVED)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@Throws(NoSuchMessageException::class)
|
||||
private fun getMessageExportState(messageId: MessageId): MessageExportState {
|
||||
return readableDatabase
|
||||
|
||||
@@ -113,6 +113,8 @@ public interface MessageTypes {
|
||||
long SPECIAL_TYPE_GIFT_BADGE = 0x200000000L;
|
||||
long SPECIAL_TYPE_PAYMENTS_NOTIFICATION = 0x300000000L;
|
||||
long SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST = 0x400000000L;
|
||||
long SPECIAL_TYPE_REPORTED_SPAM = 0x500000000L;
|
||||
long SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED = 0x600000000L;
|
||||
long SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L;
|
||||
|
||||
long IGNORABLE_TYPESMASK_WHEN_COUNTING = END_SESSION_BIT | KEY_EXCHANGE_IDENTITY_UPDATE_BIT | KEY_EXCHANGE_IDENTITY_VERIFIED_BIT;
|
||||
@@ -137,6 +139,14 @@ public interface MessageTypes {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_PAYMENTS_ACTIVATED;
|
||||
}
|
||||
|
||||
static boolean isReportedSpam(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_REPORTED_SPAM;
|
||||
}
|
||||
|
||||
static boolean isMessageRequestAccepted(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_MESSAGE_REQUEST_ACCEPTED;
|
||||
}
|
||||
|
||||
static boolean isDraftMessageType(long type) {
|
||||
return (type & BASE_TYPE_MASK) == BASE_DRAFT_TYPE;
|
||||
}
|
||||
|
||||
@@ -239,4 +239,12 @@ public abstract class DisplayRecord {
|
||||
public boolean isPaymentsActivated() {
|
||||
return MessageTypes.isPaymentsActivated(type);
|
||||
}
|
||||
|
||||
public boolean isReportedSpam() {
|
||||
return MessageTypes.isReportedSpam(type);
|
||||
}
|
||||
|
||||
public boolean isMessageRequestAccepted() {
|
||||
return MessageTypes.isMessageRequestAccepted(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,45 +83,6 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning message to show during message request state if you do not have groups in common
|
||||
* with an individual or do not know anyone in the group.
|
||||
*/
|
||||
public static final class NoGroupsInCommon extends InMemoryMessageRecord {
|
||||
private final boolean isGroup;
|
||||
|
||||
public NoGroupsInCommon(long threadId, boolean isGroup) {
|
||||
super(NO_GROUPS_IN_COMMON_ID, "", Recipient.UNKNOWN, threadId, 0);
|
||||
this.isGroup = isGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
|
||||
return UpdateDescription.staticDescription(context.getString(isGroup ? R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully
|
||||
: R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully),
|
||||
R.drawable.symbol_info_compact_16);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showActionButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isGroup() {
|
||||
return isGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @StringRes int getActionButtonText() {
|
||||
return R.string.ConversationUpdateItem_learn_more;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RemovedContactHidden extends InMemoryMessageRecord {
|
||||
|
||||
public RemovedContactHidden(long threadId) {
|
||||
|
||||
@@ -271,6 +271,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
} else if (isPaymentsActivated()) {
|
||||
return isOutgoing() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_activated_payments), R.drawable.ic_card_activate_payments)
|
||||
: fromRecipient(getFromRecipient(), r -> context.getString(R.string.MessageRecord_can_accept_payments, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments);
|
||||
} else if (isReportedSpam()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_reported_as_spam), R.drawable.symbol_spam_16);
|
||||
} else if (isMessageRequestAccepted()) {
|
||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_you_accepted_the_message_request), R.drawable.symbol_thread_16);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -632,7 +636,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
|
||||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() ||
|
||||
isPaymentsRequestToActivate() || isPaymentsActivated();
|
||||
isPaymentsRequestToActivate() || isPaymentsActivated() || isReportedSpam() || isMessageRequestAccepted();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
|
||||
@@ -200,11 +200,12 @@ public final class GroupManager {
|
||||
@Nullable GroupSecretParams groupSecretParams,
|
||||
int revision,
|
||||
long timestamp,
|
||||
@Nullable byte[] signedGroupChange)
|
||||
@Nullable byte[] signedGroupChange,
|
||||
@Nullable String serverGuid)
|
||||
throws GroupChangeBusyException, IOException, GroupNotAMemberException
|
||||
{
|
||||
try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
|
||||
return updater.updateLocalToServerRevision(revision, timestamp, groupRecord, groupSecretParams, signedGroupChange);
|
||||
return updater.updateLocalToServerRevision(revision, timestamp, groupRecord, groupSecretParams, signedGroupChange, serverGuid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -805,11 +805,16 @@ final class GroupManagerV2 {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
GroupsV2StateProcessor.GroupUpdateResult updateLocalToServerRevision(int revision, long timestamp, @NonNull Optional<GroupRecord> localRecord, @Nullable GroupSecretParams groupSecretParams, @Nullable byte[] signedGroupChange)
|
||||
GroupsV2StateProcessor.GroupUpdateResult updateLocalToServerRevision(int revision,
|
||||
long timestamp,
|
||||
@NonNull Optional<GroupRecord> localRecord,
|
||||
@Nullable GroupSecretParams groupSecretParams,
|
||||
@Nullable byte[] signedGroupChange,
|
||||
@Nullable String serverGuid)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
return new GroupsV2StateProcessor(context).forGroup(serviceIds, groupMasterKey, groupSecretParams)
|
||||
.updateLocalGroupToRevision(revision, timestamp, localRecord, getDecryptedGroupChange(signedGroupChange));
|
||||
.updateLocalGroupToRevision(revision, timestamp, localRecord, getDecryptedGroupChange(signedGroupChange), serverGuid);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
||||
@@ -238,10 +238,10 @@ public class GroupsV2StateProcessor {
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
if (localState.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
info("Inserting single update message for restore placeholder");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)), null);
|
||||
} else {
|
||||
info("Inserting force update messages");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries(), null);
|
||||
}
|
||||
profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState);
|
||||
|
||||
@@ -259,7 +259,7 @@ public class GroupsV2StateProcessor {
|
||||
@Nullable DecryptedGroupChange signedGroupChange)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
return updateLocalGroupToRevision(revision, timestamp, groupDatabase.getGroup(groupId), signedGroupChange);
|
||||
return updateLocalGroupToRevision(revision, timestamp, groupDatabase.getGroup(groupId), signedGroupChange, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +271,8 @@ public class GroupsV2StateProcessor {
|
||||
public GroupUpdateResult updateLocalGroupToRevision(final int revision,
|
||||
final long timestamp,
|
||||
@NonNull Optional<GroupRecord> localRecord,
|
||||
@Nullable DecryptedGroupChange signedGroupChange)
|
||||
@Nullable DecryptedGroupChange signedGroupChange,
|
||||
@Nullable String serverGuid)
|
||||
throws IOException, GroupNotAMemberException
|
||||
{
|
||||
if (localIsAtLeast(localRecord, revision)) {
|
||||
@@ -287,7 +288,6 @@ public class GroupsV2StateProcessor {
|
||||
localState.revision + 1 == signedGroupChange.revision &&
|
||||
revision == signedGroupChange.revision)
|
||||
{
|
||||
|
||||
if (notInGroupAndNotBeingAdded(localRecord, signedGroupChange) && notHavingInviteRevoked(signedGroupChange)) {
|
||||
warn("Ignoring P2P group change because we're not currently in the group and this change doesn't add us in. Falling back to a server fetch.");
|
||||
} else if (SignalStore.internalValues().gv2IgnoreP2PChanges()) {
|
||||
@@ -306,7 +306,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
if (inputGroupState == null) {
|
||||
try {
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, false);
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, false, serverGuid);
|
||||
} catch (GroupNotAMemberException e) {
|
||||
if (localState != null && signedGroupChange != null) {
|
||||
try {
|
||||
@@ -346,9 +346,9 @@ public class GroupsV2StateProcessor {
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
if (localState != null && localState.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
info("Inserting single update message for restore placeholder");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)));
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(newLocalState, null)), null);
|
||||
} else {
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries(), serverGuid);
|
||||
}
|
||||
profileAndMessageHelper.persistLearnedProfileKeys(inputGroupState);
|
||||
|
||||
@@ -398,7 +398,7 @@ public class GroupsV2StateProcessor {
|
||||
/**
|
||||
* Using network, attempt to bring the local copy of the group up to the revision specified via paging.
|
||||
*/
|
||||
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst) throws IOException, GroupNotAMemberException {
|
||||
private GroupUpdateResult updateLocalGroupFromServerPaged(int revision, DecryptedGroup localState, long timestamp, boolean forceIncludeFirst, @Nullable String serverGuid) throws IOException, GroupNotAMemberException {
|
||||
boolean latestRevisionOnly = revision == LATEST && (localState == null || localState.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION);
|
||||
|
||||
info("Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly);
|
||||
@@ -456,7 +456,7 @@ public class GroupsV2StateProcessor {
|
||||
int requestRevision = (revision == LATEST) ? latestServerGroup.getRevision() : revision;
|
||||
if (newLocalRevision < requestRevision) {
|
||||
warn( "Paging again with force first snapshot enabled due to error processing changes. New local revision [" + newLocalRevision + "] hasn't reached our desired level [" + requestRevision + "]");
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, true);
|
||||
return updateLocalGroupFromServerPaged(revision, localState, timestamp, true, serverGuid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ public class GroupsV2StateProcessor {
|
||||
updateLocalDatabaseGroupState(inputGroupState, newLocalState);
|
||||
|
||||
if (localState == null || localState.revision != GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
timestamp = profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries());
|
||||
timestamp = profileAndMessageHelper.insertUpdateMessages(timestamp, localState, advanceGroupStateResult.getProcessedLogEntries(), serverGuid);
|
||||
}
|
||||
|
||||
for (ServerGroupLogEntry entry : inputGroupState.getServerHistory()) {
|
||||
@@ -491,7 +491,7 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
if (localState != null && localState.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) {
|
||||
info("Inserting single update message for restore placeholder");
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null)));
|
||||
profileAndMessageHelper.insertUpdateMessages(timestamp, null, Collections.singleton(new LocalGroupLogEntry(finalState, null)), serverGuid);
|
||||
}
|
||||
|
||||
profileAndMessageHelper.persistLearnedProfileKeys(profileKeys);
|
||||
@@ -735,7 +735,8 @@ public class GroupsV2StateProcessor {
|
||||
|
||||
long insertUpdateMessages(long timestamp,
|
||||
@Nullable DecryptedGroup previousGroupState,
|
||||
Collection<LocalGroupLogEntry> processedLogEntries)
|
||||
Collection<LocalGroupLogEntry> processedLogEntries,
|
||||
@Nullable String serverGuid)
|
||||
{
|
||||
for (LocalGroupLogEntry entry : processedLogEntries) {
|
||||
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(entry.getChange()) && !DecryptedGroupUtil.changeIsEmpty(entry.getChange())) {
|
||||
@@ -746,7 +747,7 @@ public class GroupsV2StateProcessor {
|
||||
if (entry.getChange() != null && DecryptedGroupUtil.changeIsEmpty(entry.getChange()) && previousGroupState != null) {
|
||||
Log.w(TAG, "Empty group update message seen. Not inserting.");
|
||||
} else {
|
||||
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(previousGroupState, entry.getChange(), entry.getGroup()), null), timestamp);
|
||||
storeMessage(GroupProtoUtil.createDecryptedGroupV2Context(masterKey, new GroupMutation(previousGroupState, entry.getChange(), entry.getGroup()), null), timestamp, serverGuid);
|
||||
timestamp++;
|
||||
}
|
||||
}
|
||||
@@ -782,7 +783,7 @@ public class GroupsV2StateProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) {
|
||||
void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp, @Nullable String serverGuid) {
|
||||
Optional<ServiceId> editor = getEditor(decryptedGroupV2Context);
|
||||
|
||||
boolean outgoing = !editor.isPresent() || aci.equals(editor.get());
|
||||
@@ -806,7 +807,7 @@ public class GroupsV2StateProcessor {
|
||||
try {
|
||||
MessageTable smsDatabase = SignalDatabase.messages();
|
||||
RecipientId sender = RecipientId.from(editor.get());
|
||||
IncomingMessage groupMessage = IncomingMessage.groupUpdate(sender, timestamp, groupId, decryptedGroupV2Context);
|
||||
IncomingMessage groupMessage = IncomingMessage.groupUpdate(sender, timestamp, groupId, decryptedGroupV2Context, serverGuid);
|
||||
Optional<MessageTable.InsertResult> insertResult = smsDatabase.insertMessageInbox(groupMessage);
|
||||
|
||||
if (insertResult.isPresent()) {
|
||||
|
||||
@@ -54,7 +54,11 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
}
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forBlockAndReportSpam(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK);
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.BLOCK_AND_SPAM);
|
||||
}
|
||||
|
||||
public static @NonNull MultiDeviceMessageRequestResponseJob forReportSpam(@NonNull RecipientId threadRecipient) {
|
||||
return new MultiDeviceMessageRequestResponseJob(threadRecipient, Type.SPAM);
|
||||
}
|
||||
|
||||
private MultiDeviceMessageRequestResponseJob(@NonNull RecipientId threadRecipient, @NonNull Type type) {
|
||||
@@ -135,6 +139,10 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
return MessageRequestResponseMessage.Type.BLOCK;
|
||||
case BLOCK_AND_DELETE:
|
||||
return MessageRequestResponseMessage.Type.BLOCK_AND_DELETE;
|
||||
case SPAM:
|
||||
return MessageRequestResponseMessage.Type.SPAM;
|
||||
case BLOCK_AND_SPAM:
|
||||
return MessageRequestResponseMessage.Type.BLOCK_AND_SPAM;
|
||||
default:
|
||||
return MessageRequestResponseMessage.Type.UNKNOWN;
|
||||
}
|
||||
@@ -151,7 +159,7 @@ public class MultiDeviceMessageRequestResponseJob extends BaseJob {
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
UNKNOWN(0), ACCEPT(1), DELETE(2), BLOCK(3), BLOCK_AND_DELETE(4);
|
||||
UNKNOWN(0), ACCEPT(1), DELETE(2), BLOCK(3), BLOCK_AND_DELETE(4), SPAM(5), BLOCK_AND_SPAM(6);
|
||||
|
||||
private final int value;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulRespons
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -72,9 +73,28 @@ public class ReportSpamJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
List<ReportSpamData> reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT);
|
||||
SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
|
||||
if (threadRecipient == null) {
|
||||
Log.w(TAG, "No recipient for thread");
|
||||
return;
|
||||
}
|
||||
|
||||
List<ReportSpamData> reportSpamData;
|
||||
|
||||
if (threadRecipient.isGroup()) {
|
||||
Recipient inviter = SignalDatabase.groups().getGroupInviter(threadRecipient.requireGroupId());
|
||||
if (inviter == null) {
|
||||
Log.w(TAG, "Unable to determine inviter to report");
|
||||
return;
|
||||
}
|
||||
|
||||
reportSpamData = SignalDatabase.messages().getGroupReportSpamMessageServerData(threadId, inviter.getId(), timestamp, MAX_MESSAGE_COUNT);
|
||||
} else {
|
||||
reportSpamData = SignalDatabase.messages().getReportSpamMessageServerData(threadId, timestamp, MAX_MESSAGE_COUNT);
|
||||
}
|
||||
|
||||
int count = 0;
|
||||
SignalServiceAccountManager signalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
|
||||
for (ReportSpamData data : reportSpamData) {
|
||||
RecipientId recipientId = data.getRecipientId();
|
||||
@@ -88,7 +108,7 @@ public class ReportSpamJob extends BaseJob {
|
||||
if (reportingTokenBytes != null) {
|
||||
reportingTokenEncoded = Base64.encodeWithPadding(reportingTokenBytes);
|
||||
}
|
||||
|
||||
|
||||
signalServiceAccountManager.reportSpam(serviceId.get(), data.getServerGuid(), reportingTokenEncoded);
|
||||
count++;
|
||||
} else {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public final class GroupInfo {
|
||||
public static final GroupInfo ZERO = new GroupInfo(0, 0, "");
|
||||
|
||||
private final int fullMemberCount;
|
||||
private final int pendingMemberCount;
|
||||
private final String description;
|
||||
|
||||
public GroupInfo(int fullMemberCount, int pendingMemberCount, @NonNull String description) {
|
||||
this.fullMemberCount = fullMemberCount;
|
||||
this.pendingMemberCount = pendingMemberCount;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getFullMemberCount() {
|
||||
return fullMemberCount;
|
||||
}
|
||||
|
||||
public int getPendingMemberCount() {
|
||||
return pendingMemberCount;
|
||||
}
|
||||
|
||||
public @NonNull String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.thoughtcrime.securesms.messagerequests
|
||||
|
||||
/**
|
||||
* Group info needed to show message request state UX.
|
||||
*/
|
||||
class GroupInfo(
|
||||
val fullMemberCount: Int = 0,
|
||||
val pendingMemberCount: Int = 0,
|
||||
val description: String = "",
|
||||
val hasExistingContacts: Boolean = false
|
||||
) {
|
||||
companion object {
|
||||
@JvmField
|
||||
val ZERO = GroupInfo()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.Context;
|
||||
|
||||
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;
|
||||
@@ -24,12 +23,13 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReportSpamJob;
|
||||
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
|
||||
|
||||
@@ -37,7 +37,9 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
@@ -54,28 +56,6 @@ public final class MessageRequestRepository {
|
||||
this.executor = SignalExecutors.BOUNDED;
|
||||
}
|
||||
|
||||
public void getGroups(@NonNull RecipientId recipientId, @NonNull Consumer<List<String>> onGroupsLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupTable groupDatabase = SignalDatabase.groups();
|
||||
onGroupsLoaded.accept(groupDatabase.getPushGroupNamesContainingMember(recipientId));
|
||||
});
|
||||
}
|
||||
|
||||
public void getGroupInfo(@NonNull RecipientId recipientId, @NonNull Consumer<GroupInfo> onGroupInfoLoaded) {
|
||||
executor.execute(() -> {
|
||||
GroupTable groupDatabase = SignalDatabase.groups();
|
||||
Optional<GroupRecord> groupRecord = groupDatabase.getGroup(recipientId);
|
||||
onGroupInfoLoaded.accept(groupRecord.map(record -> {
|
||||
if (record.isV2Group()) {
|
||||
DecryptedGroup decryptedGroup = record.requireV2GroupProperties().getDecryptedGroup();
|
||||
return new GroupInfo(decryptedGroup.members.size(), decryptedGroup.pendingMembers.size(), decryptedGroup.description);
|
||||
} else {
|
||||
return new GroupInfo(record.getMembers().size(), 0, "");
|
||||
}
|
||||
}).orElse(GroupInfo.ZERO));
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull MessageRequestRecipientInfo getRecipientInfo(@NonNull RecipientId recipientId, long threadId) {
|
||||
List<String> sharedGroups = SignalDatabase.groups().getPushGroupNamesContainingMember(recipientId);
|
||||
@@ -83,11 +63,20 @@ public final class MessageRequestRepository {
|
||||
GroupInfo groupInfo = GroupInfo.ZERO;
|
||||
|
||||
if (groupRecord.isPresent()) {
|
||||
boolean groupHasExistingContacts = false;
|
||||
if (groupRecord.get().isV2Group()) {
|
||||
List<Recipient> recipients = Recipient.resolvedList(groupRecord.get().getMembers());
|
||||
for (Recipient recipient : recipients) {
|
||||
if ((recipient.isProfileSharing() || recipient.hasGroupsInCommon()) && !recipient.isSelf()) {
|
||||
groupHasExistingContacts = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DecryptedGroup decryptedGroup = groupRecord.get().requireV2GroupProperties().getDecryptedGroup();
|
||||
groupInfo = new GroupInfo(decryptedGroup.members.size(), decryptedGroup.pendingMembers.size(), decryptedGroup.description);
|
||||
groupInfo = new GroupInfo(decryptedGroup.members.size(), decryptedGroup.pendingMembers.size(), decryptedGroup.description, groupHasExistingContacts);
|
||||
} else {
|
||||
groupInfo = new GroupInfo(groupRecord.get().getMembers().size(), 0, "");
|
||||
groupInfo = new GroupInfo(groupRecord.get().getMembers().size(), 0, "", false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +93,11 @@ public final class MessageRequestRepository {
|
||||
@WorkerThread
|
||||
public @NonNull MessageRequestState getMessageRequestState(@NonNull Recipient recipient, long threadId) {
|
||||
if (recipient.isBlocked()) {
|
||||
boolean reportedAsSpam = reportedAsSpam(threadId);
|
||||
if (recipient.isGroup()) {
|
||||
return MessageRequestState.BLOCKED_GROUP;
|
||||
return new MessageRequestState(MessageRequestState.State.BLOCKED_GROUP, reportedAsSpam);
|
||||
} else {
|
||||
return MessageRequestState.BLOCKED_INDIVIDUAL;
|
||||
return new MessageRequestState(MessageRequestState.State.INDIVIDUAL_BLOCKED, reportedAsSpam);
|
||||
}
|
||||
} else if (threadId <= 0) {
|
||||
return MessageRequestState.NONE;
|
||||
@@ -115,45 +105,56 @@ public final class MessageRequestRepository {
|
||||
switch (getGroupMemberLevel(recipient.getId())) {
|
||||
case NOT_A_MEMBER:
|
||||
return MessageRequestState.NONE;
|
||||
case PENDING_MEMBER:
|
||||
return MessageRequestState.GROUP_V2_INVITE;
|
||||
default:
|
||||
case PENDING_MEMBER: {
|
||||
boolean reportedAsSpam = reportedAsSpam(threadId);
|
||||
return new MessageRequestState(MessageRequestState.State.GROUP_V2_INVITE, reportedAsSpam);
|
||||
}
|
||||
default: {
|
||||
if (RecipientUtil.isMessageRequestAccepted(context, threadId)) {
|
||||
return MessageRequestState.NONE;
|
||||
} else {
|
||||
return MessageRequestState.GROUP_V2_ADD;
|
||||
boolean reportedAsSpam = reportedAsSpam(threadId);
|
||||
return new MessageRequestState(MessageRequestState.State.GROUP_V2_ADD, reportedAsSpam);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!RecipientUtil.isLegacyProfileSharingAccepted(recipient) && isLegacyThread(recipient)) {
|
||||
if (recipient.isGroup()) {
|
||||
return MessageRequestState.DEPRECATED_GROUP_V1;
|
||||
return MessageRequestState.DEPRECATED_V1;
|
||||
} else {
|
||||
return MessageRequestState.LEGACY_INDIVIDUAL;
|
||||
return new MessageRequestState(MessageRequestState.State.LEGACY_INDIVIDUAL);
|
||||
}
|
||||
} else if (recipient.isPushV1Group()) {
|
||||
if (RecipientUtil.isMessageRequestAccepted(context, threadId)) {
|
||||
return MessageRequestState.DEPRECATED_GROUP_V1;
|
||||
return MessageRequestState.DEPRECATED_V1;
|
||||
} else if (!recipient.isActiveGroup()) {
|
||||
return MessageRequestState.NONE;
|
||||
} else {
|
||||
return MessageRequestState.DEPRECATED_GROUP_V1;
|
||||
return MessageRequestState.DEPRECATED_V1;
|
||||
}
|
||||
} else {
|
||||
if (RecipientUtil.isMessageRequestAccepted(context, threadId)) {
|
||||
return MessageRequestState.NONE;
|
||||
} else {
|
||||
Recipient.HiddenState hiddenState = RecipientUtil.getRecipientHiddenState(threadId);
|
||||
Recipient.HiddenState hiddenState = RecipientUtil.getRecipientHiddenState(threadId);
|
||||
boolean reportedAsSpam = reportedAsSpam(threadId);
|
||||
|
||||
if (hiddenState == Recipient.HiddenState.NOT_HIDDEN) {
|
||||
return MessageRequestState.INDIVIDUAL;
|
||||
return new MessageRequestState(MessageRequestState.State.INDIVIDUAL, reportedAsSpam);
|
||||
} else if (hiddenState == Recipient.HiddenState.HIDDEN) {
|
||||
return MessageRequestState.NONE_HIDDEN;
|
||||
return new MessageRequestState(MessageRequestState.State.NONE_HIDDEN, reportedAsSpam);
|
||||
} else {
|
||||
return MessageRequestState.INDIVIDUAL_HIDDEN;
|
||||
return new MessageRequestState(MessageRequestState.State.INDIVIDUAL_HIDDEN, reportedAsSpam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean reportedAsSpam(long threadId) {
|
||||
return SignalDatabase.messages().hasReportSpamMessage(threadId) ||
|
||||
SignalDatabase.messages().getOutgoingSecureMessageCount(threadId) > 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> acceptMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
@@ -172,7 +173,7 @@ public final class MessageRequestRepository {
|
||||
@NonNull Runnable onMessageRequestAccepted,
|
||||
@NonNull GroupChangeErrorCallback error)
|
||||
{
|
||||
executor.execute(()-> {
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
if (recipient.isPushV2Group()) {
|
||||
try {
|
||||
@@ -182,6 +183,7 @@ public final class MessageRequestRepository {
|
||||
RecipientTable recipientTable = SignalDatabase.recipients();
|
||||
recipientTable.setProfileSharing(recipientId, true);
|
||||
|
||||
insertMessageRequestAccept(recipient, threadId);
|
||||
onMessageRequestAccepted.run();
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
@@ -205,11 +207,25 @@ public final class MessageRequestRepository {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipientId));
|
||||
}
|
||||
|
||||
insertMessageRequestAccept(recipient, threadId);
|
||||
onMessageRequestAccepted.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void insertMessageRequestAccept(Recipient recipient, long threadId) {
|
||||
try {
|
||||
SignalDatabase.messages().insertMessageOutbox(
|
||||
OutgoingMessage.messageRequestAcceptMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Unable to insert message request accept message", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> deleteMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
@@ -295,6 +311,18 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Completable reportSpamMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
return Completable.create(emitter -> {
|
||||
reportSpamMessageRequest(
|
||||
recipientId,
|
||||
threadId,
|
||||
emitter::onComplete
|
||||
);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> blockAndReportSpamMessageRequest(@NonNull RecipientId recipientId, long threadId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
@@ -315,13 +343,22 @@ public final class MessageRequestRepository {
|
||||
{
|
||||
executor.execute(() -> {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
try{
|
||||
try {
|
||||
RecipientUtil.block(context, recipient);
|
||||
SignalDatabase.messages().insertMessageOutbox(
|
||||
OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
);
|
||||
} catch (GroupChangeException | IOException e) {
|
||||
Log.w(TAG, e);
|
||||
error.onError(GroupChangeFailureReason.fromException(e));
|
||||
return;
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Unable to insert report spam message", e);
|
||||
}
|
||||
|
||||
Recipient.live(recipientId).refresh();
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new ReportSpamJob(threadId, System.currentTimeMillis()));
|
||||
@@ -334,6 +371,33 @@ public final class MessageRequestRepository {
|
||||
});
|
||||
}
|
||||
|
||||
private void reportSpamMessageRequest(@NonNull RecipientId recipientId,
|
||||
long threadId,
|
||||
@NonNull Runnable onReported)
|
||||
{
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
SignalDatabase.messages().insertMessageOutbox(
|
||||
OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Unable to insert report spam message", e);
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new ReportSpamJob(threadId, System.currentTimeMillis()));
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forReportSpam(recipientId));
|
||||
}
|
||||
|
||||
onReported.run();
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public @NonNull Single<Result<Unit, GroupChangeFailureReason>> unblockAndAccept(@NonNull RecipientId recipientId) {
|
||||
//noinspection CodeBlock2Expr
|
||||
@@ -361,9 +425,9 @@ public final class MessageRequestRepository {
|
||||
|
||||
private GroupTable.MemberLevel getGroupMemberLevel(@NonNull RecipientId recipientId) {
|
||||
return SignalDatabase.groups()
|
||||
.getGroup(recipientId)
|
||||
.map(g -> g.memberLevel(Recipient.self()))
|
||||
.orElse(GroupTable.MemberLevel.NOT_A_MEMBER);
|
||||
.getGroup(recipientId)
|
||||
.map(g -> g.memberLevel(Recipient.self()))
|
||||
.orElse(GroupTable.MemberLevel.NOT_A_MEMBER);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
/**
|
||||
* An enum representing the possible message request states a user can be in.
|
||||
*/
|
||||
public enum MessageRequestState {
|
||||
/** No message request necessary */
|
||||
NONE,
|
||||
|
||||
/** No message request necessary as the user was hidden after accepting*/
|
||||
NONE_HIDDEN,
|
||||
|
||||
/** A user is blocked */
|
||||
BLOCKED_INDIVIDUAL,
|
||||
|
||||
/** A group is blocked */
|
||||
BLOCKED_GROUP,
|
||||
|
||||
/** An individual conversation that existed pre-message-requests but doesn't have profile sharing enabled */
|
||||
LEGACY_INDIVIDUAL,
|
||||
|
||||
/** A V1 group conversation that is no longer allowed, because we've forced GV2 on. */
|
||||
DEPRECATED_GROUP_V1,
|
||||
|
||||
/** An invite response is needed for a V2 group */
|
||||
GROUP_V2_INVITE,
|
||||
|
||||
/** A message request is needed for a V2 group */
|
||||
GROUP_V2_ADD,
|
||||
|
||||
/** A message request is needed for an individual */
|
||||
INDIVIDUAL,
|
||||
|
||||
/** A message request is needed for an individual since they have been hidden */
|
||||
INDIVIDUAL_HIDDEN
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.thoughtcrime.securesms.messagerequests
|
||||
|
||||
/**
|
||||
* Data necessary to render message request view.
|
||||
*/
|
||||
data class MessageRequestState @JvmOverloads constructor(val state: State = State.NONE, val reportedAsSpam: Boolean = false) {
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val NONE = MessageRequestState()
|
||||
|
||||
@JvmField
|
||||
val DEPRECATED_V1 = MessageRequestState()
|
||||
}
|
||||
|
||||
val isAccepted: Boolean
|
||||
get() = state == State.NONE || state == State.NONE_HIDDEN
|
||||
|
||||
val isBlocked: Boolean
|
||||
get() = state == State.INDIVIDUAL_BLOCKED || state == State.BLOCKED_GROUP
|
||||
|
||||
/**
|
||||
* An enum representing the possible message request states a user can be in.
|
||||
*/
|
||||
enum class State {
|
||||
/** No message request necessary */
|
||||
NONE,
|
||||
|
||||
/** No message request necessary as the user was hidden after accepting */
|
||||
NONE_HIDDEN,
|
||||
|
||||
/** A group is blocked */
|
||||
BLOCKED_GROUP,
|
||||
|
||||
/** An individual conversation that existed pre-message-requests but doesn't have profile sharing enabled */
|
||||
LEGACY_INDIVIDUAL,
|
||||
|
||||
/** A V1 group conversation that is no longer allowed, because we've forced GV2 on. */
|
||||
DEPRECATED_GROUP_V1,
|
||||
|
||||
/** An invite response is needed for a V2 group */
|
||||
GROUP_V2_INVITE,
|
||||
|
||||
/** A message request is needed for a V2 group */
|
||||
GROUP_V2_ADD,
|
||||
|
||||
/** A message request is needed for an individual */
|
||||
INDIVIDUAL,
|
||||
|
||||
/** A user is blocked */
|
||||
INDIVIDUAL_BLOCKED,
|
||||
|
||||
/** A message request is needed for an individual since they have been hidden */
|
||||
INDIVIDUAL_HIDDEN
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.Store;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MessageRequestViewModel extends ViewModel {
|
||||
|
||||
private final SingleLiveEvent<Status> status = new SingleLiveEvent<>();
|
||||
private final SingleLiveEvent<GroupChangeFailureReason> failures = new SingleLiveEvent<>();
|
||||
private final MutableLiveData<Recipient> recipient = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<String>> groups = new MutableLiveData<>(Collections.emptyList());
|
||||
private final MutableLiveData<GroupInfo> groupInfo = new MutableLiveData<>(GroupInfo.ZERO);
|
||||
private final Store<RecipientInfo> recipientInfoStore = new Store<>(new RecipientInfo(null, null, null, null));
|
||||
|
||||
private final LiveData<MessageData> messageData;
|
||||
private final LiveData<RequestReviewDisplayState> requestReviewDisplayState;
|
||||
private final MessageRequestRepository repository;
|
||||
|
||||
private LiveRecipient liveRecipient;
|
||||
private long threadId;
|
||||
|
||||
private final RecipientForeverObserver recipientObserver = recipient -> {
|
||||
loadGroupInfo();
|
||||
this.recipient.setValue(recipient);
|
||||
};
|
||||
|
||||
private MessageRequestViewModel(MessageRequestRepository repository) {
|
||||
this.repository = repository;
|
||||
this.messageData = LiveDataUtil.mapAsync(recipient, this::createMessageDataForRecipient);
|
||||
this.requestReviewDisplayState = LiveDataUtil.mapAsync(messageData, MessageRequestViewModel::transformHolderToReviewDisplayState);
|
||||
|
||||
recipientInfoStore.update(this.recipient, (recipient, state) -> new RecipientInfo(recipient, state.groupInfo, state.sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.groupInfo, (groupInfo, state) -> new RecipientInfo(state.recipient, groupInfo, state.sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.groups, (sharedGroups, state) -> new RecipientInfo(state.recipient, state.groupInfo, sharedGroups, state.messageRequestState));
|
||||
recipientInfoStore.update(this.messageData, (messageData, state) -> new RecipientInfo(state.recipient, state.groupInfo, state.sharedGroups, messageData.messageState));
|
||||
}
|
||||
|
||||
public void setConversationInfo(@NonNull RecipientId recipientId, long threadId) {
|
||||
if (liveRecipient != null) {
|
||||
liveRecipient.removeForeverObserver(recipientObserver);
|
||||
}
|
||||
|
||||
liveRecipient = Recipient.live(recipientId);
|
||||
this.threadId = threadId;
|
||||
|
||||
loadRecipient();
|
||||
loadGroups();
|
||||
loadGroupInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (liveRecipient != null) {
|
||||
liveRecipient.removeForeverObserver(recipientObserver);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<RequestReviewDisplayState> getRequestReviewDisplayState() {
|
||||
return requestReviewDisplayState;
|
||||
}
|
||||
|
||||
public LiveData<Recipient> getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public LiveData<MessageData> getMessageData() {
|
||||
return messageData;
|
||||
}
|
||||
|
||||
public LiveData<RecipientInfo> getRecipientInfo() {
|
||||
return recipientInfoStore.getStateLiveData();
|
||||
}
|
||||
|
||||
public LiveData<Status> getMessageRequestStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public LiveData<GroupChangeFailureReason> getFailures() {
|
||||
return failures;
|
||||
}
|
||||
|
||||
public boolean shouldShowMessageRequest() {
|
||||
MessageData data = messageData.getValue();
|
||||
return data != null && data.getMessageState() != MessageRequestState.NONE;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onAccept() {
|
||||
status.setValue(Status.ACCEPTING);
|
||||
repository.acceptMessageRequest(liveRecipient.getId(),
|
||||
threadId,
|
||||
() -> status.postValue(Status.ACCEPTED),
|
||||
this::onGroupChangeError);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onDelete() {
|
||||
status.setValue(Status.DELETING);
|
||||
repository.deleteMessageRequest(liveRecipient.getId(),
|
||||
threadId,
|
||||
() -> status.postValue(Status.DELETED),
|
||||
this::onGroupChangeError);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onBlock() {
|
||||
status.setValue(Status.BLOCKING);
|
||||
repository.blockMessageRequest(liveRecipient.getId(),
|
||||
() -> status.postValue(Status.BLOCKED),
|
||||
this::onGroupChangeError);
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onUnblock() {
|
||||
repository.unblockAndAccept(liveRecipient.getId(),
|
||||
() -> status.postValue(Status.ACCEPTED));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onBlockAndReportSpam() {
|
||||
repository.blockAndReportSpamMessageRequest(liveRecipient.getId(),
|
||||
threadId,
|
||||
() -> status.postValue(Status.BLOCKED_AND_REPORTED),
|
||||
this::onGroupChangeError);
|
||||
}
|
||||
|
||||
private void onGroupChangeError(@NonNull GroupChangeFailureReason error) {
|
||||
status.postValue(Status.IDLE);
|
||||
failures.postValue(error);
|
||||
}
|
||||
|
||||
private void loadRecipient() {
|
||||
liveRecipient.observeForever(recipientObserver);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
recipient.postValue(liveRecipient.get());
|
||||
});
|
||||
}
|
||||
|
||||
private void loadGroups() {
|
||||
repository.getGroups(liveRecipient.getId(), this.groups::postValue);
|
||||
}
|
||||
|
||||
private void loadGroupInfo() {
|
||||
repository.getGroupInfo(liveRecipient.getId(), groupInfo::postValue);
|
||||
}
|
||||
|
||||
private static RequestReviewDisplayState transformHolderToReviewDisplayState(@NonNull MessageData holder) {
|
||||
if (holder.getMessageState() == MessageRequestState.INDIVIDUAL) {
|
||||
return ReviewUtil.isRecipientReviewSuggested(holder.getRecipient().getId()) ? RequestReviewDisplayState.SHOWN
|
||||
: RequestReviewDisplayState.HIDDEN;
|
||||
} else {
|
||||
return RequestReviewDisplayState.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull MessageData createMessageDataForRecipient(@NonNull Recipient recipient) {
|
||||
MessageRequestState state = repository.getMessageRequestState(recipient, threadId);
|
||||
return new MessageData(recipient, state);
|
||||
}
|
||||
|
||||
public static class RecipientInfo {
|
||||
@Nullable private final Recipient recipient;
|
||||
@NonNull private final GroupInfo groupInfo;
|
||||
@NonNull private final List<String> sharedGroups;
|
||||
@Nullable private final MessageRequestState messageRequestState;
|
||||
|
||||
public RecipientInfo(@Nullable Recipient recipient, @Nullable GroupInfo groupInfo, @Nullable List<String> sharedGroups, @Nullable MessageRequestState messageRequestState) {
|
||||
this.recipient = recipient;
|
||||
this.groupInfo = groupInfo == null ? GroupInfo.ZERO : groupInfo;
|
||||
this.sharedGroups = sharedGroups == null ? Collections.emptyList() : sharedGroups;
|
||||
this.messageRequestState = messageRequestState;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public int getGroupMemberCount() {
|
||||
return groupInfo.getFullMemberCount();
|
||||
}
|
||||
|
||||
public int getGroupPendingMemberCount() {
|
||||
return groupInfo.getPendingMemberCount();
|
||||
}
|
||||
|
||||
public @NonNull String getGroupDescription() {
|
||||
return groupInfo.getDescription();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<String> getSharedGroups() {
|
||||
return sharedGroups;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MessageRequestState getMessageRequestState() {
|
||||
return messageRequestState;
|
||||
}
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
IDLE,
|
||||
BLOCKING,
|
||||
BLOCKED,
|
||||
BLOCKED_AND_REPORTED,
|
||||
DELETING,
|
||||
DELETED,
|
||||
ACCEPTING,
|
||||
ACCEPTED
|
||||
}
|
||||
|
||||
public enum RequestReviewDisplayState {
|
||||
HIDDEN,
|
||||
SHOWN,
|
||||
NONE
|
||||
}
|
||||
|
||||
public static final class MessageData {
|
||||
private final Recipient recipient;
|
||||
private final MessageRequestState messageState;
|
||||
|
||||
public MessageData(@NonNull Recipient recipient, @NonNull MessageRequestState messageState) {
|
||||
this.recipient = recipient;
|
||||
this.messageState = messageState;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @NonNull MessageRequestState getMessageState() {
|
||||
return messageState;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public Factory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection unchecked
|
||||
return (T) new MessageRequestViewModel(new MessageRequestRepository(context.getApplicationContext()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messagerequests;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
|
||||
private final Debouncer showProgressDebouncer = new Debouncer(250);
|
||||
|
||||
private LearnMoreTextView question;
|
||||
private MaterialButton accept;
|
||||
private MaterialButton block;
|
||||
private MaterialButton delete;
|
||||
private MaterialButton bigDelete;
|
||||
private MaterialButton bigUnblock;
|
||||
private View busyIndicator;
|
||||
|
||||
private Group normalButtons;
|
||||
private Group blockedButtons;
|
||||
private @Nullable Group activeGroup;
|
||||
|
||||
public MessageRequestsBottomView(Context context) {
|
||||
super(context);
|
||||
onFinishInflate();
|
||||
}
|
||||
|
||||
public MessageRequestsBottomView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MessageRequestsBottomView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
inflate(getContext(), R.layout.message_request_bottom_bar, this);
|
||||
|
||||
question = findViewById(R.id.message_request_question);
|
||||
accept = findViewById(R.id.message_request_accept);
|
||||
block = findViewById(R.id.message_request_block);
|
||||
delete = findViewById(R.id.message_request_delete);
|
||||
bigDelete = findViewById(R.id.message_request_big_delete);
|
||||
bigUnblock = findViewById(R.id.message_request_big_unblock);
|
||||
normalButtons = findViewById(R.id.message_request_normal_buttons);
|
||||
blockedButtons = findViewById(R.id.message_request_blocked_buttons);
|
||||
busyIndicator = findViewById(R.id.message_request_busy_indicator);
|
||||
|
||||
setWallpaperEnabled(false);
|
||||
}
|
||||
|
||||
public void setMessageData(@NonNull MessageRequestViewModel.MessageData messageData) {
|
||||
Recipient recipient = messageData.getRecipient();
|
||||
|
||||
question.setLearnMoreVisible(false);
|
||||
question.setOnLinkClickListener(null);
|
||||
|
||||
switch (messageData.getMessageState()) {
|
||||
case BLOCKED_INDIVIDUAL:
|
||||
int message = recipient.isReleaseNotes() ? R.string.MessageRequestBottomView_get_updates_and_news_from_s_you_wont_receive_any_updates_until_you_unblock_them
|
||||
: recipient.isRegistered() ? R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them
|
||||
: R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them_SMS;
|
||||
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(message,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
|
||||
setActiveInactiveGroups(blockedButtons, normalButtons);
|
||||
break;
|
||||
case BLOCKED_GROUP:
|
||||
question.setText(R.string.MessageRequestBottomView_unblock_this_group_and_share_your_name_and_photo_with_its_members);
|
||||
setActiveInactiveGroups(blockedButtons, normalButtons);
|
||||
break;
|
||||
case LEGACY_INDIVIDUAL:
|
||||
question.setText(getContext().getString(R.string.MessageRequestBottomView_continue_your_conversation_with_s_and_share_your_name_and_photo, recipient.getShortDisplayName(getContext())));
|
||||
question.setLearnMoreVisible(true);
|
||||
question.setOnLinkClickListener(v -> CommunicationActions.openBrowserLink(getContext(), getContext().getString(R.string.MessageRequestBottomView_legacy_learn_more_url)));
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
accept.setText(R.string.MessageRequestBottomView_continue);
|
||||
break;
|
||||
case DEPRECATED_GROUP_V1:
|
||||
question.setText(R.string.MessageRequestBottomView_upgrade_this_group_to_activate_new_features);
|
||||
setActiveInactiveGroups(null, normalButtons, blockedButtons);
|
||||
break;
|
||||
case GROUP_V2_INVITE:
|
||||
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_you_wont_see_their_messages);
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
accept.setText(R.string.MessageRequestBottomView_accept);
|
||||
break;
|
||||
case GROUP_V2_ADD:
|
||||
question.setText(R.string.MessageRequestBottomView_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept);
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
accept.setText(R.string.MessageRequestBottomView_accept);
|
||||
break;
|
||||
case INDIVIDUAL:
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
accept.setText(R.string.MessageRequestBottomView_accept);
|
||||
break;
|
||||
case INDIVIDUAL_HIDDEN:
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_you_removed_them_before,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
|
||||
setActiveInactiveGroups(normalButtons, blockedButtons);
|
||||
accept.setText(R.string.MessageRequestBottomView_accept);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setActiveInactiveGroups(@Nullable Group activeGroup, @NonNull Group... inActiveGroups) {
|
||||
int initialVisibility = this.activeGroup != null ? this.activeGroup.getVisibility() : VISIBLE;
|
||||
|
||||
this.activeGroup = activeGroup;
|
||||
|
||||
for (Group inactive : inActiveGroups) {
|
||||
inactive.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (activeGroup != null) {
|
||||
activeGroup.setVisibility(initialVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
public void showBusy() {
|
||||
showProgressDebouncer.publish(() -> busyIndicator.setVisibility(VISIBLE));
|
||||
if (activeGroup != null) {
|
||||
activeGroup.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideBusy() {
|
||||
showProgressDebouncer.clear();
|
||||
busyIndicator.setVisibility(GONE);
|
||||
if (activeGroup != null) {
|
||||
activeGroup.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setWallpaperEnabled(boolean isEnabled) {
|
||||
MessageRequestBarColorTheme theme = MessageRequestBarColorTheme.resolveTheme(isEnabled);
|
||||
|
||||
Stream.of(delete, bigDelete, block, bigUnblock, accept).forEach(button -> {
|
||||
button.setBackgroundTintList(ColorStateList.valueOf(theme.getButtonBackgroundColor(getContext())));
|
||||
});
|
||||
|
||||
Stream.of(delete, bigDelete, block).forEach(button -> {
|
||||
button.setTextColor(theme.getButtonForegroundDenyColor(getContext()));
|
||||
});
|
||||
|
||||
Stream.of(accept, bigUnblock).forEach(button -> {
|
||||
button.setTextColor(theme.getButtonForegroundAcceptColor(getContext()));
|
||||
});
|
||||
|
||||
setBackgroundColor(theme.getContainerButtonBackgroundColor(getContext()));
|
||||
}
|
||||
|
||||
public void setAcceptOnClickListener(OnClickListener acceptOnClickListener) {
|
||||
accept.setOnClickListener(acceptOnClickListener);
|
||||
}
|
||||
|
||||
public void setDeleteOnClickListener(OnClickListener deleteOnClickListener) {
|
||||
delete.setOnClickListener(deleteOnClickListener);
|
||||
bigDelete.setOnClickListener(deleteOnClickListener);
|
||||
}
|
||||
|
||||
public void setBlockOnClickListener(OnClickListener blockOnClickListener) {
|
||||
block.setOnClickListener(blockOnClickListener);
|
||||
}
|
||||
|
||||
public void setUnblockOnClickListener(OnClickListener unblockOnClickListener) {
|
||||
bigUnblock.setOnClickListener(unblockOnClickListener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.messagerequests
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestBarColorTheme.Companion.resolveTheme
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* View shown in a conversation during a message request state or related state (e.g., blocked).
|
||||
*/
|
||||
class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
private val showProgressDebouncer = Debouncer(250)
|
||||
|
||||
private val question: LearnMoreTextView
|
||||
private val accept: MaterialButton
|
||||
private val block: MaterialButton
|
||||
private val unblock: MaterialButton
|
||||
private val delete: MaterialButton
|
||||
private val report: MaterialButton
|
||||
private val busyIndicator: View
|
||||
private val buttonBar: View
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.message_request_bottom_bar, this)
|
||||
|
||||
question = findViewById(R.id.message_request_question)
|
||||
accept = findViewById(R.id.message_request_accept)
|
||||
block = findViewById(R.id.message_request_block)
|
||||
unblock = findViewById(R.id.message_request_unblock)
|
||||
delete = findViewById(R.id.message_request_delete)
|
||||
report = findViewById(R.id.message_request_report)
|
||||
busyIndicator = findViewById(R.id.message_request_busy_indicator)
|
||||
buttonBar = findViewById(R.id.message_request_button_layout)
|
||||
|
||||
setWallpaperEnabled(false)
|
||||
}
|
||||
|
||||
fun setMessageRequestData(recipient: Recipient, messageRequestState: MessageRequestState) {
|
||||
question.setLearnMoreVisible(false)
|
||||
question.setOnLinkClickListener(null)
|
||||
|
||||
updateButtonVisibility(messageRequestState)
|
||||
|
||||
when (messageRequestState.state) {
|
||||
MessageRequestState.State.INDIVIDUAL_BLOCKED -> {
|
||||
val message = if (recipient.isReleaseNotes) R.string.MessageRequestBottomView_get_updates_and_news_from_s_you_wont_receive_any_updates_until_you_unblock_them else if (recipient.isRegistered) R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them else R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them_SMS
|
||||
question.text = HtmlCompat.fromHtml(
|
||||
context.getString(
|
||||
message,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(context))
|
||||
),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
MessageRequestState.State.BLOCKED_GROUP -> question.setText(R.string.MessageRequestBottomView_unblock_this_group_and_share_your_name_and_photo_with_its_members)
|
||||
|
||||
MessageRequestState.State.LEGACY_INDIVIDUAL -> {
|
||||
question.text = context.getString(R.string.MessageRequestBottomView_continue_your_conversation_with_s_and_share_your_name_and_photo, recipient.getShortDisplayName(context))
|
||||
question.setLearnMoreVisible(true)
|
||||
question.setOnLinkClickListener { CommunicationActions.openBrowserLink(context, context.getString(R.string.MessageRequestBottomView_legacy_learn_more_url)) }
|
||||
accept.setText(R.string.MessageRequestBottomView_continue)
|
||||
}
|
||||
|
||||
MessageRequestState.State.DEPRECATED_GROUP_V1 -> question.setText(R.string.MessageRequestBottomView_upgrade_this_group_to_activate_new_features)
|
||||
MessageRequestState.State.GROUP_V2_INVITE -> {
|
||||
question.setText(R.string.MessageRequestBottomView_do_you_want_to_join_this_group_you_wont_see_their_messages)
|
||||
accept.setText(R.string.MessageRequestBottomView_accept)
|
||||
}
|
||||
|
||||
MessageRequestState.State.GROUP_V2_ADD -> {
|
||||
question.setText(R.string.MessageRequestBottomView_join_this_group_they_wont_know_youve_seen_their_messages_until_you_accept)
|
||||
accept.setText(R.string.MessageRequestBottomView_accept)
|
||||
}
|
||||
|
||||
MessageRequestState.State.INDIVIDUAL -> {
|
||||
question.text = HtmlCompat.fromHtml(
|
||||
context.getString(
|
||||
R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(context))
|
||||
),
|
||||
0
|
||||
)
|
||||
accept.setText(R.string.MessageRequestBottomView_accept)
|
||||
}
|
||||
|
||||
MessageRequestState.State.INDIVIDUAL_HIDDEN -> {
|
||||
question.text = HtmlCompat.fromHtml(
|
||||
context.getString(
|
||||
R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_you_removed_them_before,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(context))
|
||||
),
|
||||
0
|
||||
)
|
||||
accept.setText(R.string.MessageRequestBottomView_accept)
|
||||
}
|
||||
|
||||
MessageRequestState.State.NONE -> Unit
|
||||
MessageRequestState.State.NONE_HIDDEN -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButtonVisibility(messageState: MessageRequestState) {
|
||||
accept.visible = !messageState.isBlocked
|
||||
block.visible = !messageState.isBlocked
|
||||
unblock.visible = messageState.isBlocked
|
||||
delete.visible = messageState.reportedAsSpam || messageState.isBlocked
|
||||
report.visible = !messageState.reportedAsSpam
|
||||
}
|
||||
|
||||
fun showBusy() {
|
||||
showProgressDebouncer.publish { busyIndicator.visibility = VISIBLE }
|
||||
buttonBar.visibility = INVISIBLE
|
||||
}
|
||||
|
||||
fun hideBusy() {
|
||||
showProgressDebouncer.clear()
|
||||
busyIndicator.visibility = GONE
|
||||
buttonBar.visibility = VISIBLE
|
||||
}
|
||||
|
||||
fun setWallpaperEnabled(isEnabled: Boolean) {
|
||||
val theme = resolveTheme(isEnabled)
|
||||
listOf(delete, block, accept, unblock, report).forEach { it.backgroundTintList = ColorStateList.valueOf(theme.getButtonBackgroundColor(context)) }
|
||||
listOf(delete, block, report).forEach { it.setTextColor(theme.getButtonForegroundDenyColor(context)) }
|
||||
listOf(accept, unblock).forEach { it.setTextColor(theme.getButtonForegroundAcceptColor(context)) }
|
||||
|
||||
setBackgroundColor(theme.getContainerButtonBackgroundColor(context))
|
||||
}
|
||||
|
||||
fun setAcceptOnClickListener(acceptOnClickListener: OnClickListener?) {
|
||||
accept.setOnClickListener(acceptOnClickListener)
|
||||
}
|
||||
|
||||
fun setDeleteOnClickListener(deleteOnClickListener: OnClickListener?) {
|
||||
delete.setOnClickListener(deleteOnClickListener)
|
||||
}
|
||||
|
||||
fun setBlockOnClickListener(blockOnClickListener: OnClickListener?) {
|
||||
block.setOnClickListener(blockOnClickListener)
|
||||
}
|
||||
|
||||
fun setUnblockOnClickListener(unblockOnClickListener: OnClickListener?) {
|
||||
unblock.setOnClickListener(unblockOnClickListener)
|
||||
}
|
||||
|
||||
fun setReportOnClickListener(reportOnClickListener: OnClickListener?) {
|
||||
report.setOnClickListener(reportOnClickListener)
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,18 @@ object DataMessageProcessor {
|
||||
|
||||
var groupProcessResult: MessageContentProcessor.Gv2PreProcessResult? = null
|
||||
if (groupId != null) {
|
||||
groupProcessResult = MessageContentProcessor.handleGv2PreProcessing(context, envelope.timestamp!!, content, metadata, groupId, message.groupV2!!, senderRecipient, groupSecretParams)
|
||||
groupProcessResult = MessageContentProcessor.handleGv2PreProcessing(
|
||||
context = context,
|
||||
timestamp = envelope.timestamp!!,
|
||||
content = content,
|
||||
metadata = metadata,
|
||||
groupId = groupId,
|
||||
groupV2 = message.groupV2!!,
|
||||
senderRecipient = senderRecipient,
|
||||
groupSecretParams = groupSecretParams,
|
||||
serverGuid = envelope.serverGuid
|
||||
)
|
||||
|
||||
if (groupProcessResult == MessageContentProcessor.Gv2PreProcessResult.IGNORE) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,10 +228,11 @@ open class MessageContentProcessor(private val context: Context) {
|
||||
groupId: GroupId.V2,
|
||||
groupV2: GroupContextV2,
|
||||
senderRecipient: Recipient,
|
||||
groupSecretParams: GroupSecretParams? = null
|
||||
groupSecretParams: GroupSecretParams? = null,
|
||||
serverGuid: String? = null
|
||||
): Gv2PreProcessResult {
|
||||
val preUpdateGroupRecord = SignalDatabase.groups.getGroup(groupId)
|
||||
val groupUpdateResult = updateGv2GroupFromServerOrP2PChange(context, timestamp, groupV2, preUpdateGroupRecord, groupSecretParams)
|
||||
val groupUpdateResult = updateGv2GroupFromServerOrP2PChange(context, timestamp, groupV2, preUpdateGroupRecord, groupSecretParams, serverGuid)
|
||||
if (groupUpdateResult == null) {
|
||||
log(timestamp, "Ignoring GV2 message for group we are not currently in $groupId")
|
||||
return Gv2PreProcessResult.IGNORE
|
||||
@@ -272,13 +273,14 @@ open class MessageContentProcessor(private val context: Context) {
|
||||
timestamp: Long,
|
||||
groupV2: GroupContextV2,
|
||||
localRecord: Optional<GroupRecord>,
|
||||
groupSecretParams: GroupSecretParams? = null
|
||||
groupSecretParams: GroupSecretParams? = null,
|
||||
serverGuid: String? = null
|
||||
): GroupsV2StateProcessor.GroupUpdateResult? {
|
||||
return try {
|
||||
val signedGroupChange: ByteArray? = if (groupV2.hasSignedGroupChange) groupV2.signedGroupChange else null
|
||||
val updatedTimestamp = if (signedGroupChange != null) timestamp else timestamp - 1
|
||||
if (groupV2.revision != null) {
|
||||
GroupManager.updateGroupFromServer(context, groupV2.groupMasterKey, localRecord, groupSecretParams, groupV2.revision!!, updatedTimestamp, signedGroupChange)
|
||||
GroupManager.updateGroupFromServer(context, groupV2.groupMasterKey, localRecord, groupSecretParams, groupV2.revision!!, updatedTimestamp, signedGroupChange, serverGuid)
|
||||
} else {
|
||||
warn(timestamp, "Ignore group update message without a revision")
|
||||
null
|
||||
|
||||
@@ -73,9 +73,6 @@ import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toSignalServic
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.type
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage.Companion.endSessionMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage.Companion.expirationUpdateMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage.Companion.text
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
|
||||
import org.thoughtcrime.securesms.payments.MobileCoinPublicAddress
|
||||
@@ -122,6 +119,7 @@ import org.whispersystems.signalservice.internal.push.Verified
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration
|
||||
|
||||
object SyncMessageProcessor {
|
||||
@@ -576,7 +574,7 @@ object SyncMessageProcessor {
|
||||
log(envelopeTimestamp, "Synchronize end session message.")
|
||||
|
||||
val recipient: Recipient = getSyncMessageDestination(sent)
|
||||
val outgoingEndSessionMessage: OutgoingMessage = endSessionMessage(recipient, sent.timestamp!!)
|
||||
val outgoingEndSessionMessage: OutgoingMessage = OutgoingMessage.endSessionMessage(recipient, sent.timestamp!!)
|
||||
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
@@ -624,7 +622,7 @@ object SyncMessageProcessor {
|
||||
}
|
||||
|
||||
val recipient: Recipient = getSyncMessageDestination(sent)
|
||||
val expirationUpdateMessage: OutgoingMessage = expirationUpdateMessage(recipient, if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!, sent.message!!.expireTimerDuration.inWholeMilliseconds)
|
||||
val expirationUpdateMessage: OutgoingMessage = OutgoingMessage.expirationUpdateMessage(recipient, if (sideEffect) sent.timestamp!! - 1 else sent.timestamp!!, sent.message!!.expireTimerDuration.inWholeMilliseconds)
|
||||
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val messageId: Long = SignalDatabase.messages.insertMessageOutbox(expirationUpdateMessage, threadId, false, null)
|
||||
|
||||
@@ -830,7 +828,7 @@ object SyncMessageProcessor {
|
||||
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
|
||||
updateGroupReceiptStatus(sent, messageId, recipient.requireGroupId())
|
||||
} else {
|
||||
val outgoingTextMessage = text(threadRecipient = recipient, body = body, expiresIn = expiresInMillis, sentTimeMillis = sent.timestamp!!, bodyRanges = bodyRanges)
|
||||
val outgoingTextMessage = OutgoingMessage.text(threadRecipient = recipient, body = body, expiresIn = expiresInMillis, sentTimeMillis = sent.timestamp!!, bodyRanges = bodyRanges)
|
||||
messageId = SignalDatabase.messages.insertMessageOutbox(outgoingTextMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markUnidentified(messageId, sent.isUnidentified(recipient.serviceId.orNull()))
|
||||
}
|
||||
@@ -1058,6 +1056,12 @@ object SyncMessageProcessor {
|
||||
MessageRequestResponse.Type.ACCEPT -> {
|
||||
SignalDatabase.recipients.setProfileSharing(recipient.id, true)
|
||||
SignalDatabase.recipients.setBlocked(recipient.id, false)
|
||||
SignalDatabase.messages.insertMessageOutbox(
|
||||
OutgoingMessage.messageRequestAcceptMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
}
|
||||
MessageRequestResponse.Type.DELETE -> {
|
||||
SignalDatabase.recipients.setProfileSharing(recipient.id, false)
|
||||
@@ -1076,6 +1080,24 @@ object SyncMessageProcessor {
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
MessageRequestResponse.Type.SPAM -> {
|
||||
SignalDatabase.messages.insertMessageOutbox(
|
||||
OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
}
|
||||
MessageRequestResponse.Type.BLOCK_AND_SPAM -> {
|
||||
SignalDatabase.recipients.setBlocked(recipient.id, true)
|
||||
SignalDatabase.recipients.setProfileSharing(recipient.id, false)
|
||||
SignalDatabase.messages.insertMessageOutbox(
|
||||
OutgoingMessage.reportSpamMessage(recipient, System.currentTimeMillis(), TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong())),
|
||||
threadId,
|
||||
false,
|
||||
null
|
||||
)
|
||||
}
|
||||
else -> warn("Got an unknown response type! Skipping")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class IncomingMessage(
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun groupUpdate(from: RecipientId, timestamp: Long, groupId: GroupId, groupContext: DecryptedGroupV2Context): IncomingMessage {
|
||||
fun groupUpdate(from: RecipientId, timestamp: Long, groupId: GroupId, groupContext: DecryptedGroupV2Context, serverGuid: String?): IncomingMessage {
|
||||
val messageGroupContext = MessageGroupContext(groupContext)
|
||||
|
||||
return IncomingMessage(
|
||||
@@ -104,6 +104,7 @@ class IncomingMessage(
|
||||
serverTimeMillis = timestamp,
|
||||
groupId = groupId,
|
||||
groupContext = messageGroupContext,
|
||||
serverGuid = serverGuid,
|
||||
body = messageGroupContext.encodedGroupContext,
|
||||
type = MessageType.GROUP_UPDATE
|
||||
)
|
||||
|
||||
@@ -50,7 +50,9 @@ data class OutgoingMessage(
|
||||
val isIdentityVerified: Boolean = false,
|
||||
val isIdentityDefault: Boolean = false,
|
||||
val scheduledDate: Long = -1,
|
||||
val messageToEdit: Long = 0
|
||||
val messageToEdit: Long = 0,
|
||||
val isReportSpam: Boolean = false,
|
||||
val isMessageRequestAccept: Boolean = false
|
||||
) {
|
||||
|
||||
val isV2Group: Boolean = messageGroupContext != null && GroupV2UpdateMessageUtil.isGroupV2(messageGroupContext)
|
||||
@@ -401,6 +403,30 @@ data class OutgoingMessage(
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun reportSpamMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
expiresIn = expiresIn,
|
||||
isReportSpam = true,
|
||||
isUrgent = false,
|
||||
isSecure = true
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun messageRequestAcceptMessage(threadRecipient: Recipient, sentTimeMillis: Long, expiresIn: Long): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
expiresIn = expiresIn,
|
||||
isMessageRequestAccept = true,
|
||||
isUrgent = false,
|
||||
isSecure = true
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun buildMessage(slideDeck: SlideDeck, message: String): String {
|
||||
return if (message.isNotEmpty() && slideDeck.body.isNotEmpty()) {
|
||||
|
||||
Reference in New Issue
Block a user