CFV2 Save to Disk / Copy Text Content.

This commit is contained in:
Alex Hart
2023-05-26 15:51:30 -03:00
committed by Cody Henthorne
parent 399421e20e
commit b785b3f887
13 changed files with 554 additions and 26 deletions

View File

@@ -33,7 +33,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat;
import com.annimon.stream.Stream;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log;
import org.signal.glide.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
@@ -394,7 +394,8 @@ public final class ConversationReactionOverlay extends FrameLayout {
private void updateToolbarShade(@NonNull Activity activity) {
View toolbar = activity.findViewById(R.id.toolbar);
View bannerContainer = activity.findViewById(R.id.conversation_banner_container);
View bannerContainer = activity.findViewById(SignalStore.internalValues().useConversationFragmentV2() ? R.id.conversation_banner
: R.id.conversation_banner_container);
LayoutParams layoutParams = (LayoutParams) toolbarShade.getLayoutParams();
layoutParams.height = toolbar.getHeight() + bannerContainer.getHeight();
@@ -410,9 +411,9 @@ public final class ConversationReactionOverlay extends FrameLayout {
private int getInputPanelHeight(@NonNull Activity activity) {
if (SignalStore.internalValues().useConversationFragmentV2()) {
Barrier conversationBottomPanelBarrier = activity.findViewById(R.id.conversation_bottom_panel_barrier);
View bottomPanel = activity.findViewById(R.id.conversation_input_panel);
return activity.getResources().getDisplayMetrics().heightPixels - conversationBottomPanelBarrier.getTop();
return bottomPanel.getHeight();
}
View bottomPanel = activity.findViewById(R.id.conversation_activity_panel_parent);

View File

@@ -161,7 +161,6 @@ class MultiselectItemDecoration(
/**
* Draws the background shade.
*/
@Suppress("DEPRECATION")
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val adapter = parent.adapter as ConversationAdapterBridge

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.conversation.v2
import android.Manifest
import android.annotation.SuppressLint
import android.app.ActivityOptions
import android.content.Intent
@@ -84,6 +85,8 @@ import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
import org.thoughtcrime.securesms.components.InputPanel
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.ProgressCardDialogFragmentArgs
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.SendButton
import org.thoughtcrime.securesms.components.ViewBinderDelegate
@@ -171,6 +174,7 @@ import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.notifications.v2.ConversationId
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.Recipient
@@ -194,7 +198,9 @@ import org.thoughtcrime.securesms.util.DrawableUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import org.thoughtcrime.securesms.util.StorageUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.WindowUtil
@@ -743,9 +749,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
ConversationAdapter.initializePool(binding.conversationItemRecycler.recycledViewPool)
adapter.setPagingController(viewModel.pagingController)
binding.conversationItemRecycler.adapter = adapter
giphyMp4ProjectionRecycler = initializeGiphyMp4()
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
recyclerViewColorizer.setChatColors(args.chatColors)
binding.conversationItemRecycler.adapter = adapter
multiselectItemDecoration = MultiselectItemDecoration(
requireContext()
) { viewModel.wallpaperSnapshot }
@@ -756,12 +763,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
binding.conversationItemRecycler.addItemDecoration(multiselectItemDecoration)
viewLifecycleOwner.lifecycle.addObserver(multiselectItemDecoration)
giphyMp4ProjectionRecycler = initializeGiphyMp4()
val layoutTransitionListener = BubbleLayoutTransitionListener(binding.conversationItemRecycler)
viewLifecycleOwner.lifecycle.addObserver(layoutTransitionListener)
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
recyclerViewColorizer.setChatColors(args.chatColors)
binding.conversationItemRecycler.itemAnimator = ConversationItemAnimator(
isInMultiSelectMode = adapter.selectedItems::isNotEmpty,
shouldPlayMessageAnimations = {
@@ -1118,11 +1124,44 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
private fun handleSaveAttachment(record: MediaMmsMessageRecord) {
// TODO [cfv2] -- Not implemented yet.
if (record.isViewOnce) {
error("Cannot save a view-once message")
}
val attachments = SaveAttachmentUtil.getAttachmentsForRecord(record)
SaveAttachmentUtil.showWarningDialog(requireContext(), attachments.size) { _, _ ->
if (StorageUtil.canWriteToMediaStore()) {
performAttachmentSave(attachments)
} else {
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied { toast(R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, toastDuration = Toast.LENGTH_LONG) }
.onAllGranted { performAttachmentSave(attachments) }
.execute()
}
}
}
private fun performAttachmentSave(attachments: Set<SaveAttachmentUtil.SaveAttachment>) {
val progressDialog = ProgressCardDialogFragment()
progressDialog.arguments = ProgressCardDialogFragmentArgs.Builder(
resources.getQuantityString(R.plurals.ConversationFragment_saving_n_attachments_to_sd_card, attachments.size, attachments.size)
).build().toBundle()
SaveAttachmentUtil.saveAttachments(attachments)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { progressDialog.show(parentFragmentManager, null) }
.doOnTerminate { progressDialog.dismissAllowingStateLoss() }
.subscribeBy { it.toast(requireContext()) }
.addTo(disposables)
}
private fun handleCopyMessage(messageParts: Set<MultiselectPart>) {
// TODO [cfv2] -- Not implemented yet.
viewModel.copyToClipboard(requireContext(), messageParts).subscribe().addTo(disposables)
}
private fun handleDisplayDetails(conversationMessage: ConversationMessage) {

View File

@@ -8,12 +8,14 @@ package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.net.Uri
import android.os.Build
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.StreamUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.core.util.toOptional
@@ -27,8 +29,10 @@ import org.thoughtcrime.securesms.components.reminder.PendingGroupJoinRequestsRe
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock
import org.thoughtcrime.securesms.database.GroupTable
@@ -59,6 +63,9 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.hasTextSlide
import org.thoughtcrime.securesms.util.requireTextSlide
import java.io.IOException
import java.util.Optional
import kotlin.math.max
@@ -228,15 +235,19 @@ class ConversationRepository(
ApplicationDependencies.getJobManager().add(ServiceOutageDetectionJob())
ServiceOutageReminder()
}
groupRecord != null && groupRecord.actionableRequestingMembersCount > 0 -> {
PendingGroupJoinRequestsReminder(groupRecord.actionableRequestingMembersCount)
}
groupRecord != null && groupRecord.gv1MigrationSuggestions.isNotEmpty() -> {
GroupsV1MigrationSuggestionsReminder(groupRecord.gv1MigrationSuggestions)
}
isInBubble && !SignalStore.tooltips().hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29 -> {
BubbleOptOutReminder()
}
else -> null
}
@@ -301,6 +312,50 @@ class ConversationRepository(
}.subscribeOn(Schedulers.io())
}
/**
* Copies the selected content to the clipboard. Maybe will emit either the copied contents or
* a complete which means there were no contents to be copied.
*/
fun copyToClipboard(context: Context, messageParts: Set<MultiselectPart>): Maybe<CharSequence> {
return Maybe.fromCallable { extractBodies(context, messageParts) }
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess {
Util.copyToClipboard(context, it)
}
}
private fun extractBodies(context: Context, messageParts: Set<MultiselectPart>): CharSequence {
return messageParts
.asSequence()
.sortedBy { it.getMessageRecord().dateReceived }
.map { it.conversationMessage }
.distinct()
.mapNotNull { message ->
if (message.messageRecord.hasTextSlide()) {
val textSlideUri = message.messageRecord.requireTextSlide().uri
if (textSlideUri == null) {
message.getDisplayBody(context)
} else {
try {
PartAuthority.getAttachmentStream(context, textSlideUri).use {
val body = StreamUtil.readFullyAsString(it)
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, message.messageRecord, body, message.threadRecipient)
.getDisplayBody(context)
}
} catch (e: IOException) {
Log.w(TAG, "failed to read text slide data.")
null
}
}
} else {
message.getDisplayBody(context)
}
}
.filterNot(Util::isEmpty)
.joinToString("\n")
}
data class MessageCounts(
val unread: Int,
val mentions: Int

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@@ -27,6 +28,7 @@ import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.model.IdentityRecord
@@ -258,4 +260,8 @@ class ConversationViewModel(
fun getTemporaryViewOnceUri(mmsMessageRecord: MmsMessageRecord): Maybe<Uri> {
return repository.getTemporaryViewOnceUri(mmsMessageRecord).observeOn(AndroidSchedulers.mainThread())
}
fun copyToClipboard(context: Context, messageParts: Set<MultiselectPart>): Maybe<CharSequence> {
return repository.copyToClipboard(context, messageParts)
}
}