mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 19:56:02 +01:00
Implement Stories feature behind flag.
Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com> Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
This commit is contained in:
@@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.ConversationItemAnimator;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectItemDecoration;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
@@ -185,7 +186,7 @@ import java.util.concurrent.ExecutionException;
|
||||
import kotlin.Unit;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public class ConversationFragment extends LoggingFragment implements MultiselectForwardFragment.Callback {
|
||||
public class ConversationFragment extends LoggingFragment implements MultiselectForwardBottomSheet.Callback {
|
||||
private static final String TAG = Log.tag(ConversationFragment.class);
|
||||
|
||||
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
|
||||
@@ -1013,7 +1014,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
MultiselectForwardFragmentArgs.create(requireContext(),
|
||||
multiselectParts,
|
||||
args -> MultiselectForwardFragment.show(getChildFragmentManager(), args));
|
||||
args -> MultiselectForwardFragment.showBottomSheet(getChildFragmentManager(), args));
|
||||
}
|
||||
|
||||
private void handleResendMessage(final MessageRecord message) {
|
||||
@@ -1307,6 +1308,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissForwardSheet() {
|
||||
}
|
||||
|
||||
public interface ConversationFragmentListener extends VoiceNoteMediaControllerOwner {
|
||||
boolean isKeyboardOpen();
|
||||
|
||||
@@ -108,6 +108,7 @@ import org.thoughtcrime.securesms.PromptMmsActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.ShortcutLauncherActivity;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
@@ -300,6 +301,7 @@ import org.whispersystems.signalservice.api.SignalSessionLock;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -2955,7 +2957,7 @@ public class ConversationParentFragment extends Fragment
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, result.isStory(), null, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
@@ -3031,7 +3033,7 @@ public class ConversationParentFragment extends Fragment
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(Recipient.resolved(recipientId), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, false, null, quote, contacts, previews, mentions);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.avatar.view.AvatarView;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@@ -29,7 +30,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
private AvatarImageView avatar;
|
||||
private AvatarView avatar;
|
||||
private BadgeImageView badge;
|
||||
private TextView title;
|
||||
private TextView subtitle;
|
||||
@@ -111,7 +112,7 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
|
||||
|
||||
if (recipient != null) {
|
||||
this.avatar.setAvatar(glideRequests, recipient, false);
|
||||
this.avatar.displayChatAvatar(glideRequests, recipient, false);
|
||||
}
|
||||
|
||||
if (recipient == null || recipient.isSelf()) {
|
||||
|
||||
@@ -10,8 +10,11 @@ import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.OvalShape
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.ColorInt
|
||||
import com.google.common.base.Objects
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.util.ColorUtil
|
||||
import org.thoughtcrime.securesms.components.RotatableGradientDrawable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
|
||||
@@ -25,11 +28,12 @@ import kotlin.math.min
|
||||
* @param linearGradient The LinearGradient to render. Null if this is for a single color.
|
||||
* @param singleColor The single color to render. Null if this is for a linear gradient.
|
||||
*/
|
||||
@Parcelize
|
||||
class ChatColors private constructor(
|
||||
val id: Id,
|
||||
private val linearGradient: LinearGradient?,
|
||||
private val singleColor: Int?
|
||||
) {
|
||||
) : Parcelable {
|
||||
|
||||
fun isGradient(): Boolean = Build.VERSION.SDK_INT >= 21 && linearGradient != null
|
||||
|
||||
@@ -182,7 +186,7 @@ class ChatColors private constructor(
|
||||
ChatColors(id, null, color)
|
||||
}
|
||||
|
||||
sealed class Id(val longValue: Long) {
|
||||
sealed class Id(val longValue: Long) : Parcelable {
|
||||
/**
|
||||
* Represents user selection of 'auto'.
|
||||
*/
|
||||
@@ -211,6 +215,12 @@ class ChatColors private constructor(
|
||||
return Objects.hashCode(longValue)
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeLong(longValue)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int = 0
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forLongValue(longValue: Long): Id {
|
||||
@@ -221,14 +231,26 @@ class ChatColors private constructor(
|
||||
else -> Custom(longValue)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Id> {
|
||||
override fun createFromParcel(parcel: Parcel): Id {
|
||||
return forLongValue(parcel.readLong())
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Id?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class LinearGradient(
|
||||
val degrees: Float,
|
||||
val colors: IntArray,
|
||||
val positions: FloatArray
|
||||
) {
|
||||
) : Parcelable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.annimon.stream.Stream
|
||||
import org.signal.core.util.MapUtil
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette.Names.all
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry.FullMember
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.HashMap
|
||||
import java.util.HashSet
|
||||
|
||||
object NameColors {
|
||||
|
||||
fun createSessionMembersCache(): MutableMap<GroupId, Set<Recipient>> {
|
||||
return mutableMapOf()
|
||||
}
|
||||
|
||||
fun getNameColorsMapLiveData(
|
||||
recipientId: LiveData<RecipientId>,
|
||||
sessionMemberCache: MutableMap<GroupId, Set<Recipient>>
|
||||
): LiveData<Map<RecipientId, NameColor>> {
|
||||
val recipient = Transformations.switchMap(recipientId) { r: RecipientId? -> Recipient.live(r!!).liveData }
|
||||
val group = Transformations.map(recipient) { obj: Recipient -> obj.groupId }
|
||||
val groupMembers = Transformations.switchMap(group) { g: Optional<GroupId> ->
|
||||
g.transform { groupId: GroupId -> this.getSessionGroupRecipients(groupId, sessionMemberCache) }
|
||||
.or { DefaultValueLiveData(emptySet()) }
|
||||
}
|
||||
return Transformations.map(groupMembers) { members: Set<Recipient>? ->
|
||||
val sorted = Stream.of(members)
|
||||
.filter { member: Recipient? -> member != Recipient.self() }
|
||||
.sortBy { obj: Recipient -> obj.requireStringId() }
|
||||
.toList()
|
||||
val names = all
|
||||
val colors: MutableMap<RecipientId, NameColor> = HashMap()
|
||||
for (i in sorted.indices) {
|
||||
colors[sorted[i].id] = names[i % names.size]
|
||||
}
|
||||
colors
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSessionGroupRecipients(groupId: GroupId, sessionMemberCache: MutableMap<GroupId, Set<Recipient>>): LiveData<Set<Recipient>> {
|
||||
val fullMembers = Transformations.map(
|
||||
LiveGroup(groupId).fullMembers
|
||||
) { members: List<FullMember>? ->
|
||||
Stream.of(members)
|
||||
.map { it.member }
|
||||
.toList()
|
||||
}
|
||||
return Transformations.map(fullMembers) { currentMembership: List<Recipient>? ->
|
||||
val cachedMembers: MutableSet<Recipient> = MapUtil.getOrDefault(sessionMemberCache, groupId, HashSet()).toMutableSet()
|
||||
cachedMembers.addAll(currentMembership!!)
|
||||
sessionMemberCache[groupId] = cachedMembers
|
||||
cachedMembers
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(), MultiselectForwardFragment.Callback {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.67f
|
||||
|
||||
private var callback: Callback? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.multiselect_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = findListener<Callback>()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val fragment = MultiselectForwardFragment()
|
||||
fragment.arguments = requireArguments()
|
||||
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.multiselect_container, fragment)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContainer(): ViewGroup {
|
||||
return requireView().parent.parent.parent as ViewGroup
|
||||
}
|
||||
|
||||
override fun setResult(bundle: Bundle) {
|
||||
setFragmentResult(MultiselectForwardFragment.RESULT_SELECTION, bundle)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
callback?.onDismissForwardSheet()
|
||||
}
|
||||
|
||||
override fun onFinishForwardAction() {
|
||||
callback?.onFinishForwardAction()
|
||||
}
|
||||
|
||||
override fun exitFlow() {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onSearchInputFocused() {
|
||||
(requireDialog() as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onFinishForwardAction()
|
||||
fun onDismissForwardSheet()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -9,67 +8,56 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
|
||||
import org.thoughtcrime.securesms.contacts.HeaderAction
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
|
||||
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment
|
||||
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.function.Consumer
|
||||
|
||||
private const val ARG_MULTISHARE_ARGS = "multiselect.forward.fragment.arg.multishare.args"
|
||||
private const val ARG_CAN_SEND_TO_NON_PUSH = "multiselect.forward.fragment.arg.can.send.to.non.push"
|
||||
private const val ARG_TITLE = "multiselect.forward.fragment.title"
|
||||
private val TAG = Log.tag(MultiselectForwardFragment::class.java)
|
||||
|
||||
class MultiselectForwardFragment :
|
||||
FixedRoundedCornerBottomSheetDialogFragment(),
|
||||
ContactSelectionListFragment.OnContactSelectedListener,
|
||||
ContactSelectionListFragment.OnSelectionLimitReachedListener,
|
||||
SafetyNumberChangeDialog.Callback {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.67f
|
||||
Fragment(),
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
ChooseStoryTypeBottomSheet.Callback {
|
||||
|
||||
private val viewModel: MultiselectForwardViewModel by viewModels(factoryProducer = this::createViewModelFactory)
|
||||
private val disposables = LifecycleDisposable()
|
||||
|
||||
private lateinit var selectionFragment: ContactSelectionListFragment
|
||||
private lateinit var contactFilterView: ContactFilterView
|
||||
private lateinit var addMessage: EditText
|
||||
private lateinit var contactSearchMediator: ContactSearchMediator
|
||||
|
||||
private var callback: Callback? = null
|
||||
|
||||
private lateinit var callback: Callback
|
||||
private var dismissibleDialog: SimpleProgressDialog.DismissibleDialog? = null
|
||||
|
||||
private var handler: Handler? = null
|
||||
|
||||
private fun createViewModelFactory(): MultiselectForwardViewModel.Factory {
|
||||
@@ -79,63 +67,44 @@ class MultiselectForwardFragment :
|
||||
private fun getMultiShareArgs(): ArrayList<MultiShareArgs> = requireNotNull(requireArguments().getParcelableArrayList(ARG_MULTISHARE_ARGS))
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
childFragmentManager.addFragmentOnAttachListener { _, fragment ->
|
||||
fragment.arguments = Bundle().apply {
|
||||
putInt(ContactSelectionListFragment.DISPLAY_MODE, getDefaultDisplayMode())
|
||||
putBoolean(ContactSelectionListFragment.REFRESHABLE, false)
|
||||
putBoolean(ContactSelectionListFragment.RECENTS, true)
|
||||
putParcelable(ContactSelectionListFragment.SELECTION_LIMITS, FeatureFlags.shareSelectionLimit())
|
||||
putBoolean(ContactSelectionListFragment.HIDE_COUNT, true)
|
||||
putBoolean(ContactSelectionListFragment.DISPLAY_CHIPS, false)
|
||||
putBoolean(ContactSelectionListFragment.CAN_SELECT_SELF, true)
|
||||
putBoolean(ContactSelectionListFragment.RV_CLIP, false)
|
||||
putInt(ContactSelectionListFragment.RV_PADDING_BOTTOM, ViewUtil.dpToPx(48))
|
||||
}
|
||||
}
|
||||
|
||||
val view = inflater.inflate(R.layout.multiselect_forward_fragment, container, false)
|
||||
|
||||
view.minimumHeight = resources.displayMetrics.heightPixels
|
||||
|
||||
return view
|
||||
return inflater.inflate(R.layout.multiselect_forward_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
callback = findListener()
|
||||
disposables.bindTo(viewLifecycleOwner.lifecycle)
|
||||
view.minimumHeight = resources.displayMetrics.heightPixels
|
||||
|
||||
selectionFragment = childFragmentManager.findFragmentById(R.id.contact_selection_list_fragment) as ContactSelectionListFragment
|
||||
val contactSearchRecycler: RecyclerView = view.findViewById(R.id.contact_selection_list)
|
||||
contactSearchMediator = ContactSearchMediator(this, contactSearchRecycler, FeatureFlags.shareSelectionLimit(), this::getConfiguration)
|
||||
|
||||
callback = findListener()!!
|
||||
disposables.bindTo(viewLifecycleOwner.lifecycle)
|
||||
|
||||
contactFilterView = view.findViewById(R.id.contact_filter_edit_text)
|
||||
|
||||
contactFilterView.setOnSearchInputFocusChangedListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
(requireDialog() as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
callback.onSearchInputFocused()
|
||||
}
|
||||
}
|
||||
|
||||
contactFilterView.setOnFilterChangedListener {
|
||||
if (it.isNullOrEmpty()) {
|
||||
selectionFragment.resetQueryFilter()
|
||||
} else {
|
||||
selectionFragment.setQueryFilter(it)
|
||||
}
|
||||
contactSearchMediator.onFilterChanged(it)
|
||||
}
|
||||
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val container = view.parent.parent.parent as FrameLayout
|
||||
val title: TextView? = view.findViewById(R.id.title)
|
||||
val container = callback.getContainer()
|
||||
val bottomBar = LayoutInflater.from(requireContext()).inflate(R.layout.multiselect_forward_fragment_bottom_bar, container, false)
|
||||
val shareSelectionRecycler: RecyclerView = bottomBar.findViewById(R.id.selected_list)
|
||||
val shareSelectionAdapter = ShareSelectionAdapter()
|
||||
val sendButton: View = bottomBar.findViewById(R.id.share_confirm)
|
||||
|
||||
title.setText(requireArguments().getInt(ARG_TITLE))
|
||||
title?.setText(requireArguments().getInt(ARG_TITLE))
|
||||
|
||||
addMessage = bottomBar.findViewById(R.id.add_message)
|
||||
|
||||
sendButton.setOnClickListener {
|
||||
sendButton.isEnabled = false
|
||||
viewModel.send(addMessage.text.toString())
|
||||
viewModel.send(addMessage.text.toString(), contactSearchMediator.getSelectedContacts())
|
||||
}
|
||||
|
||||
shareSelectionRecycler.adapter = shareSelectionAdapter
|
||||
@@ -144,8 +113,8 @@ class MultiselectForwardFragment :
|
||||
|
||||
container.addView(bottomBar)
|
||||
|
||||
viewModel.shareContactMappingModels.observe(viewLifecycleOwner) {
|
||||
shareSelectionAdapter.submitList(it)
|
||||
contactSearchMediator.getSelectionState().observe(viewLifecycleOwner) {
|
||||
shareSelectionAdapter.submitList(it.mapIndexed { index, key -> ShareSelectionMappingModel(key.requireShareContact(), index == 0) })
|
||||
|
||||
if (it.isNotEmpty() && !bottomBar.isVisible) {
|
||||
bottomBar.animation = AnimationUtils.loadAnimation(requireContext(), R.anim.slide_fade_from_bottom)
|
||||
@@ -158,7 +127,7 @@ class MultiselectForwardFragment :
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
when (it.stage) {
|
||||
MultiselectForwardState.Stage.Selection -> { }
|
||||
MultiselectForwardState.Stage.Selection -> {}
|
||||
MultiselectForwardState.Stage.FirstConfirmation -> displayFirstSendConfirmation()
|
||||
is MultiselectForwardState.Stage.SafetyConfirmation -> displaySafetyNumberConfirmation(it.stage.identities)
|
||||
MultiselectForwardState.Stage.LoadingIdentities -> {}
|
||||
@@ -170,17 +139,27 @@ class MultiselectForwardFragment :
|
||||
MultiselectForwardState.Stage.SomeFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
|
||||
MultiselectForwardState.Stage.AllFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_failed_to_send)
|
||||
MultiselectForwardState.Stage.Success -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
|
||||
is MultiselectForwardState.Stage.SelectionConfirmed -> dismissWithResult(it.stage.recipients)
|
||||
is MultiselectForwardState.Stage.SelectionConfirmed -> dismissWithSelection(it.stage.selectedContacts)
|
||||
}
|
||||
|
||||
sendButton.isEnabled = it.stage == MultiselectForwardState.Stage.Selection
|
||||
}
|
||||
|
||||
bottomBar.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
|
||||
selectionFragment.setRecyclerViewPaddingBottom(bottom - top)
|
||||
addMessage.visible = getMultiShareArgs().isNotEmpty()
|
||||
|
||||
setFragmentResultListener(CreateStoryWithViewersFragment.REQUEST_KEY) { _, bundle ->
|
||||
val recipientId: RecipientId = bundle.getParcelable(CreateStoryWithViewersFragment.STORY_RECIPIENT)!!
|
||||
contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.Story(recipientId)))
|
||||
contactFilterView.clear()
|
||||
}
|
||||
|
||||
addMessage.visible = getMultiShareArgs().isNotEmpty()
|
||||
setFragmentResultListener(ChooseGroupStoryBottomSheet.GROUP_STORY) { _, bundle ->
|
||||
val groups: Set<RecipientId> = bundle.getParcelableArrayList<RecipientId>(ChooseGroupStoryBottomSheet.RESULT_SET)?.toSet() ?: emptySet()
|
||||
val keys: Set<ContactSearchKey.Story> = groups.map { ContactSearchKey.Story(it) }.toSet()
|
||||
contactSearchMediator.addToVisibleGroupStories(keys)
|
||||
contactSearchMediator.setKeysSelected(keys)
|
||||
contactFilterView.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -207,9 +186,9 @@ class MultiselectForwardFragment :
|
||||
handler?.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
override fun onDestroyView() {
|
||||
dismissibleDialog?.dismissNow()
|
||||
super.onDismiss(dialog)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun displayFirstSendConfirmation() {
|
||||
@@ -222,7 +201,7 @@ class MultiselectForwardFragment :
|
||||
.setMessage(R.string.MultiselectForwardFragment__forwarded_messages_are_now)
|
||||
.setPositiveButton(resources.getQuantityString(R.plurals.MultiselectForwardFragment_send_d_messages, messageCount, messageCount)) { d, _ ->
|
||||
d.dismiss()
|
||||
viewModel.confirmFirstSend(addMessage.text.toString())
|
||||
viewModel.confirmFirstSend(addMessage.text.toString(), contactSearchMediator.getSelectedContacts())
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ ->
|
||||
d.dismiss()
|
||||
@@ -238,84 +217,35 @@ class MultiselectForwardFragment :
|
||||
private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) {
|
||||
val argCount = getMessageCount()
|
||||
|
||||
callback?.onFinishForwardAction()
|
||||
callback.onFinishForwardAction()
|
||||
dismissibleDialog?.dismiss()
|
||||
Toast.makeText(requireContext(), requireContext().resources.getQuantityString(toastTextResId, argCount), Toast.LENGTH_SHORT).show()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
private fun dismissWithResult(recipientIds: List<RecipientId>) {
|
||||
callback?.onFinishForwardAction()
|
||||
dismissibleDialog?.dismiss()
|
||||
setFragmentResult(
|
||||
RESULT_SELECTION,
|
||||
Bundle().apply {
|
||||
putParcelableArrayList(RESULT_SELECTION_RECIPIENTS, ArrayList(recipientIds))
|
||||
}
|
||||
)
|
||||
dismissAllowingStateLoss()
|
||||
callback.exitFlow()
|
||||
}
|
||||
|
||||
private fun getMessageCount(): Int = getMultiShareArgs().size + if (addMessage.text.isNotEmpty()) 1 else 0
|
||||
|
||||
private fun handleMessageExpired() {
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
callback?.onFinishForwardAction()
|
||||
callback.onFinishForwardAction()
|
||||
dismissibleDialog?.dismiss()
|
||||
Toast.makeText(requireContext(), resources.getQuantityString(R.plurals.MultiselectForwardFragment__couldnt_forward_messages, getMultiShareArgs().size), Toast.LENGTH_LONG).show()
|
||||
callback.exitFlow()
|
||||
}
|
||||
|
||||
private fun getDefaultDisplayMode(): Int {
|
||||
var mode = ContactsCursorLoader.DisplayMode.FLAG_PUSH or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_ACTIVE_GROUPS or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_SELF or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_NEW or
|
||||
ContactsCursorLoader.DisplayMode.FLAG_HIDE_RECENT_HEADER
|
||||
private fun dismissWithSelection(selectedContacts: Set<ContactSearchKey>) {
|
||||
callback.onFinishForwardAction()
|
||||
dismissibleDialog?.dismiss()
|
||||
|
||||
if (Util.isDefaultSmsProvider(requireContext()) && requireArguments().getBoolean(ARG_CAN_SEND_TO_NON_PUSH)) {
|
||||
mode = mode or ContactsCursorLoader.DisplayMode.FLAG_SMS
|
||||
val resultsBundle = Bundle().apply {
|
||||
putParcelableArrayList(RESULT_SELECTION_RECIPIENTS, ArrayList(selectedContacts.map { it.requireParcelable() }))
|
||||
}
|
||||
|
||||
return mode or ContactsCursorLoader.DisplayMode.FLAG_HIDE_GROUPS_V1
|
||||
}
|
||||
|
||||
override fun onBeforeContactSelected(recipientId: Optional<RecipientId>, number: String?, callback: Consumer<Boolean>) {
|
||||
if (recipientId.isPresent) {
|
||||
disposables.add(
|
||||
viewModel.addSelectedContact(recipientId, null)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { success ->
|
||||
if (!success) {
|
||||
Toast.makeText(requireContext(), R.string.ShareActivity_you_do_not_have_permission_to_send_to_this_group, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
callback.accept(success)
|
||||
contactFilterView.clear()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "Rejecting non-present recipient. Can't forward to an unknown contact.")
|
||||
callback.accept(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId>, number: String?) {
|
||||
viewModel.removeSelectedContact(recipientId, null)
|
||||
}
|
||||
|
||||
override fun onSelectionChanged() {
|
||||
}
|
||||
|
||||
override fun onSuggestedLimitReached(limit: Int) {
|
||||
}
|
||||
|
||||
override fun onHardLimitReached(limit: Int) {
|
||||
Toast.makeText(requireContext(), R.string.MultiselectForwardFragment__limit_reached, Toast.LENGTH_SHORT).show()
|
||||
callback.setResult(resultsBundle)
|
||||
callback.exitFlow()
|
||||
}
|
||||
|
||||
override fun onSendAnywayAfterSafetyNumberChange(changedRecipients: MutableList<RecipientId>) {
|
||||
viewModel.confirmSafetySend(addMessage.text.toString())
|
||||
viewModel.confirmSafetySend(addMessage.text.toString(), contactSearchMediator.getSelectedContacts())
|
||||
}
|
||||
|
||||
override fun onMessageResentAfterSafetyNumberChange() {
|
||||
@@ -326,14 +256,98 @@ class MultiselectForwardFragment :
|
||||
viewModel.cancelSend()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getHeaderAction(): HeaderAction {
|
||||
return HeaderAction(
|
||||
R.string.ContactsCursorLoader_new_story,
|
||||
R.drawable.ic_plus_20
|
||||
) {
|
||||
ChooseStoryTypeBottomSheet().show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(contactSearchState: ContactSearchState): ContactSearchConfiguration {
|
||||
return ContactSearchConfiguration.build {
|
||||
query = contactSearchState.query
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Stories(
|
||||
groupStories = contactSearchState.groupStories,
|
||||
includeHeader = true,
|
||||
headerAction = getHeaderAction(),
|
||||
expandConfig = ContactSearchConfiguration.ExpandConfig(
|
||||
isExpanded = contactSearchState.expandedSections.contains(ContactSearchConfiguration.SectionKey.STORIES)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (query.isNullOrEmpty()) {
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Recents(
|
||||
includeHeader = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Individuals(
|
||||
includeHeader = true,
|
||||
transportType = if (includeSms()) ContactSearchConfiguration.TransportType.ALL else ContactSearchConfiguration.TransportType.PUSH,
|
||||
includeSelf = true
|
||||
)
|
||||
)
|
||||
|
||||
addSection(
|
||||
ContactSearchConfiguration.Section.Groups(
|
||||
includeHeader = true,
|
||||
includeMms = includeSms()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun includeSms(): Boolean {
|
||||
return Util.isDefaultSmsProvider(requireContext()) && requireArguments().getBoolean(ARG_CAN_SEND_TO_NON_PUSH)
|
||||
}
|
||||
|
||||
override fun onGroupStoryClicked() {
|
||||
ChooseGroupStoryBottomSheet().show(parentFragmentManager, ChooseGroupStoryBottomSheet.GROUP_STORY)
|
||||
}
|
||||
|
||||
override fun onNewStoryClicked() {
|
||||
CreateStoryFlowDialogFragment().show(parentFragmentManager, CreateStoryWithViewersFragment.REQUEST_KEY)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onFinishForwardAction()
|
||||
fun exitFlow()
|
||||
fun onSearchInputFocused()
|
||||
fun setResult(bundle: Bundle)
|
||||
fun getContainer(): ViewGroup
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_MULTISHARE_ARGS = "multiselect.forward.fragment.arg.multishare.args"
|
||||
const val ARG_CAN_SEND_TO_NON_PUSH = "multiselect.forward.fragment.arg.can.send.to.non.push"
|
||||
const val ARG_TITLE = "multiselect.forward.fragment.title"
|
||||
const val RESULT_SELECTION = "result_selection"
|
||||
const val RESULT_SELECTION_RECIPIENTS = "result_selection_recipients"
|
||||
|
||||
@JvmStatic
|
||||
fun show(supportFragmentManager: FragmentManager, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) {
|
||||
val fragment = MultiselectForwardFragment()
|
||||
fun showBottomSheet(supportFragmentManager: FragmentManager, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) {
|
||||
val fragment = MultiselectForwardBottomSheet()
|
||||
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelableArrayList(ARG_MULTISHARE_ARGS, ArrayList(multiselectForwardFragmentArgs.multiShareArgs))
|
||||
putBoolean(ARG_CAN_SEND_TO_NON_PUSH, multiselectForwardFragmentArgs.canSendToNonPush)
|
||||
putInt(ARG_TITLE, multiselectForwardFragmentArgs.title)
|
||||
}
|
||||
|
||||
fragment.show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showFullScreen(supportFragmentManager: FragmentManager, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) {
|
||||
val fragment = MultiselectForwardFullScreenDialogFragment()
|
||||
|
||||
fragment.arguments = Bundle().apply {
|
||||
putParcelableArrayList(ARG_MULTISHARE_ARGS, ArrayList(multiselectForwardFragmentArgs.multiShareArgs))
|
||||
@@ -344,8 +358,4 @@ class MultiselectForwardFragment :
|
||||
fragment.show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onFinishForwardAction()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), MultiselectForwardFragment.Callback {
|
||||
override fun getTitle(): Int = R.string.MediaReviewFragment__send_to
|
||||
|
||||
override fun getDialogLayoutResource(): Int = R.layout.fragment_container
|
||||
|
||||
override fun onFinishForwardAction() {
|
||||
findListener<Callback>()?.onFinishForwardAction()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (savedInstanceState == null) {
|
||||
val fragment = MultiselectForwardFragment()
|
||||
fragment.arguments = requireArguments()
|
||||
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContainer(): ViewGroup {
|
||||
return requireView().findViewById(R.id.full_screen_dialog_content) as ViewGroup
|
||||
}
|
||||
|
||||
override fun setResult(bundle: Bundle) {
|
||||
setFragmentResult(MultiselectForwardFragment.RESULT_SELECTION, bundle)
|
||||
}
|
||||
|
||||
override fun exitFlow() {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onSearchInputFocused() = Unit
|
||||
|
||||
interface Callback {
|
||||
fun onFinishForwardAction()
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import androidx.core.util.Consumer
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
@@ -13,7 +14,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareSender
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
import org.thoughtcrime.securesms.sharing.ShareContactAndThread
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
@@ -27,9 +27,11 @@ class MultiselectForwardRepository(context: Context) {
|
||||
val onAllMessagesFailed: () -> Unit
|
||||
)
|
||||
|
||||
fun checkForBadIdentityRecords(shareContacts: List<ShareContact>, consumer: Consumer<List<IdentityRecord>>) {
|
||||
fun checkForBadIdentityRecords(contactSearchKeys: Set<ContactSearchKey>, consumer: Consumer<List<IdentityRecord>>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipients: List<Recipient> = shareContacts.map { Recipient.resolved(it.recipientId.get()) }
|
||||
val recipients: List<Recipient> = contactSearchKeys
|
||||
.filterIsInstance<ContactSearchKey.KnownRecipient>()
|
||||
.map { Recipient.resolved(it.recipientId) }
|
||||
val identityRecordList: IdentityRecordList = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(recipients)
|
||||
|
||||
consumer.accept(identityRecordList.untrustedRecords)
|
||||
@@ -55,7 +57,7 @@ class MultiselectForwardRepository(context: Context) {
|
||||
fun send(
|
||||
additionalMessage: String,
|
||||
multiShareArgs: List<MultiShareArgs>,
|
||||
shareContacts: List<ShareContact>,
|
||||
shareContacts: Set<ContactSearchKey>,
|
||||
resultHandlers: MultiselectForwardResultHandlers
|
||||
) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
@@ -63,10 +65,13 @@ class MultiselectForwardRepository(context: Context) {
|
||||
|
||||
val sharedContactsAndThreads: Set<ShareContactAndThread> = shareContacts
|
||||
.asSequence()
|
||||
.distinct()
|
||||
.filter { it.recipientId.isPresent }
|
||||
.map { Recipient.resolved(it.recipientId.get()) }
|
||||
.map { ShareContactAndThread(it.id, threadDatabase.getOrCreateThreadIdFor(it), it.isForceSmsSelection) }
|
||||
.filter { it is ContactSearchKey.Story || it is ContactSearchKey.KnownRecipient }
|
||||
.map {
|
||||
val recipient = Recipient.resolved(it.requireShareContact().recipientId.get())
|
||||
val isStory = it is ContactSearchKey.Story || recipient.isDistributionList
|
||||
val thread = if (isStory) -1L else threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||
ShareContactAndThread(recipient.id, thread, recipient.isForceSmsSelection, it is ContactSearchKey.Story)
|
||||
}
|
||||
.toSet()
|
||||
|
||||
val mappedArgs: List<MultiShareArgs> = multiShareArgs.map { it.buildUpon(sharedContactsAndThreads).build() }
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
|
||||
data class MultiselectForwardState(
|
||||
val selectedContacts: List<ShareContact> = emptyList(),
|
||||
val stage: Stage = Stage.Selection
|
||||
) {
|
||||
sealed class Stage {
|
||||
@@ -17,6 +15,6 @@ data class MultiselectForwardState(
|
||||
object SomeFailed : Stage()
|
||||
object AllFailed : Stage()
|
||||
object Success : Stage()
|
||||
data class SelectionConfirmed(val recipients: List<RecipientId>) : Stage()
|
||||
data class SelectionConfirmed(val selectedContacts: Set<ContactSearchKey>) : Stage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.thoughtcrime.securesms.conversation.mutiselect.forward
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sharing.MultiShareArgs
|
||||
import org.thoughtcrime.securesms.sharing.ShareContact
|
||||
import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
class MultiselectForwardViewModel(
|
||||
private val records: List<MultiShareArgs>,
|
||||
@@ -22,31 +17,15 @@ class MultiselectForwardViewModel(
|
||||
|
||||
val state: LiveData<MultiselectForwardState> = store.stateLiveData
|
||||
|
||||
val shareContactMappingModels: LiveData<List<ShareSelectionMappingModel>> = Transformations.map(state) { s -> s.selectedContacts.mapIndexed { i, c -> ShareSelectionMappingModel(c, i == 0) } }
|
||||
|
||||
fun addSelectedContact(recipientId: Optional<RecipientId>, number: String?): Single<Boolean> {
|
||||
return repository
|
||||
.canSelectRecipient(recipientId)
|
||||
.doOnSuccess { allowed ->
|
||||
if (allowed) {
|
||||
store.update { it.copy(selectedContacts = it.selectedContacts + ShareContact(recipientId, number)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSelectedContact(recipientId: Optional<RecipientId>, number: String?) {
|
||||
store.update { it.copy(selectedContacts = it.selectedContacts - ShareContact(recipientId, number)) }
|
||||
}
|
||||
|
||||
fun send(additionalMessage: String) {
|
||||
fun send(additionalMessage: String, selectedContacts: Set<ContactSearchKey>) {
|
||||
if (SignalStore.tooltips().showMultiForwardDialog()) {
|
||||
SignalStore.tooltips().markMultiForwardDialogSeen()
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.FirstConfirmation) }
|
||||
} else {
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.LoadingIdentities) }
|
||||
repository.checkForBadIdentityRecords(store.state.selectedContacts) { identityRecords ->
|
||||
repository.checkForBadIdentityRecords(selectedContacts) { identityRecords ->
|
||||
if (identityRecords.isEmpty()) {
|
||||
performSend(additionalMessage)
|
||||
performSend(additionalMessage, selectedContacts)
|
||||
} else {
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.SafetyConfirmation(identityRecords)) }
|
||||
}
|
||||
@@ -54,33 +33,27 @@ class MultiselectForwardViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmFirstSend(additionalMessage: String) {
|
||||
send(additionalMessage)
|
||||
fun confirmFirstSend(additionalMessage: String, selectedContacts: Set<ContactSearchKey>) {
|
||||
send(additionalMessage, selectedContacts)
|
||||
}
|
||||
|
||||
fun confirmSafetySend(additionalMessage: String) {
|
||||
send(additionalMessage)
|
||||
fun confirmSafetySend(additionalMessage: String, selectedContacts: Set<ContactSearchKey>) {
|
||||
send(additionalMessage, selectedContacts)
|
||||
}
|
||||
|
||||
fun cancelSend() {
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.Selection) }
|
||||
}
|
||||
|
||||
private fun performSend(additionalMessage: String) {
|
||||
private fun performSend(additionalMessage: String, selectedContacts: Set<ContactSearchKey>) {
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.SendPending) }
|
||||
if (records.isEmpty()) {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
stage = MultiselectForwardState.Stage.SelectionConfirmed(
|
||||
state.selectedContacts.filter { it.recipientId.isPresent }.map { it.recipientId.get() }.distinct()
|
||||
)
|
||||
)
|
||||
}
|
||||
store.update { it.copy(stage = MultiselectForwardState.Stage.SelectionConfirmed(selectedContacts)) }
|
||||
} else {
|
||||
repository.send(
|
||||
additionalMessage = additionalMessage,
|
||||
multiShareArgs = records,
|
||||
shareContacts = store.state.selectedContacts,
|
||||
shareContacts = selectedContacts,
|
||||
MultiselectForwardRepository.MultiselectForwardResultHandlers(
|
||||
onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.Success) } },
|
||||
onAllMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.AllFailed) } },
|
||||
|
||||
Reference in New Issue
Block a user