Display message text in Media Preview.

main
Nicholas 2023-01-24 09:11:29 -05:00 zatwierdzone przez Greyson Parrelli
rodzic 1b7e4e047c
commit eaeeb08987
9 zmienionych plików z 145 dodań i 34 usunięć

Wyświetl plik

@ -2,23 +2,17 @@ package org.thoughtcrime.securesms.longmessage;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.TextSlide;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
class LongMessageRepository {
@ -40,15 +34,9 @@ class LongMessageRepository {
@WorkerThread
private Optional<LongMessage> getMmsLongMessage(@NonNull Context context, @NonNull MessageTable mmsDatabase, long messageId) {
Optional<MmsMessageRecord> record = getMmsMessage(mmsDatabase, messageId);
if (record.isPresent()) {
TextSlide textSlide = record.get().getSlideDeck().getTextSlide();
if (textSlide != null && textSlide.getUri() != null) {
return Optional.of(new LongMessage(ConversationMessageFactory.createWithUnresolvedData(context, record.get(), readFullBody(context, textSlide.getUri()))));
} else {
return Optional.of(new LongMessage(ConversationMessageFactory.createWithUnresolvedData(context, record.get())));
}
final ConversationMessage resolvedMessage = LongMessageResolveerKt.resolveBody(record.get(), context);
return Optional.of(new LongMessage(resolvedMessage));
} else {
return Optional.empty();
}
@ -61,14 +49,6 @@ class LongMessageRepository {
}
}
private @NonNull String readFullBody(@NonNull Context context, @NonNull Uri uri) {
try (InputStream stream = PartAuthority.getAttachmentStream(context, uri)) {
return StreamUtil.readFullyAsString(stream);
} catch (IOException e) {
Log.w(TAG, "Failed to read full text body.", e);
return "";
}
}
interface Callback<T> {
void onComplete(T result);

Wyświetl plik

@ -0,0 +1,31 @@
package org.thoughtcrime.securesms.longmessage
import android.content.Context
import android.net.Uri
import org.signal.core.util.StreamUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.PartAuthority
import java.io.IOException
const val TAG = "LongMessageResolver"
fun readFullBody(context: Context, uri: Uri): String {
try {
PartAuthority.getAttachmentStream(context, uri).use { stream -> return StreamUtil.readFullyAsString(stream) }
} catch (e: IOException) {
Log.w(TAG, "Failed to read full text body.", e)
return ""
}
}
fun MmsMessageRecord.resolveBody(context: Context): ConversationMessage {
val textSlide = slideDeck.textSlide
val textSlideUri = textSlide?.uri
return if (textSlide != null && textSlideUri != null) {
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this, readFullBody(context, textSlideUri))
} else {
ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, this)
}
}

Wyświetl plik

@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.mediapreview
import android.content.Context
import android.content.Intent
import android.text.SpannableString
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
@ -19,6 +20,8 @@ import org.thoughtcrime.securesms.database.MediaTable.Sorting
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.longmessage.resolveBody
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.AttachmentUtil
@ -38,7 +41,7 @@ class MediaPreviewRepository {
* @param sorting the ordering of the results
* @param limit the maximum quantity of the results
*/
fun getAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable<Result> {
fun getAttachments(context: Context, startingAttachmentId: AttachmentId, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable<Result> {
return Single.fromCallable {
media.getGalleryMediaForThread(threadId, sorting).use { cursor ->
val mediaRecords = mutableListOf<MediaTable.MediaRecord>()
@ -71,7 +74,12 @@ class MediaPreviewRepository {
}
}
}
Result(itemPosition, mediaRecords.toList())
val messageIds = mediaRecords.mapNotNull { it.attachment?.mmsId }.toSet()
val messages: Map<Long, SpannableString> = SignalDatabase.messages.getMessages(messageIds)
.map { it as MmsMessageRecord }
.associate { it.id to it.resolveBody(context).getDisplayBody(context) }
Result(itemPosition, mediaRecords.toList(), messages)
}
}.subscribeOn(Schedulers.io()).toFlowable()
}
@ -110,5 +118,5 @@ class MediaPreviewRepository {
.observeOn(AndroidSchedulers.mainThread())
}
data class Result(val initialPosition: Int, val records: List<MediaTable.MediaRecord>)
data class Result(val initialPosition: Int, val records: List<MediaTable.MediaRecord>, val messageBodies: Map<Long, SpannableString>)
}

Wyświetl plik

@ -9,6 +9,7 @@ import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.os.Bundle
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.Menu
@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
import org.thoughtcrime.securesms.mediapreview.caption.ExpandingCaptionView
import org.thoughtcrime.securesms.mediapreview.mediarail.CenterDecoration
import org.thoughtcrime.securesms.mediapreview.mediarail.MediaRailAdapter
import org.thoughtcrime.securesms.mediapreview.mediarail.MediaRailAdapter.ImageLoadingListener
@ -127,7 +129,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
}
viewModel.initialize(args.showThread, args.allMediaInRail, args.leftIsRecent)
val sorting = MediaTable.Sorting.deserialize(args.sorting.ordinal)
viewModel.fetchAttachments(PartAuthority.requireAttachmentId(args.initialMediaUri), args.threadId, sorting)
viewModel.fetchAttachments(requireContext(), PartAuthority.requireAttachmentId(args.initialMediaUri), args.threadId, sorting)
}
@SuppressLint("RestrictedApi")
@ -231,7 +233,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
}
}
bindTextViews(currentItem, currentState.showThread)
bindTextViews(currentItem, currentState.showThread, currentState.messageBodies)
bindMenuItems(currentItem)
bindMediaPreviewPlaybackControls(currentItem, getMediaPreviewFragmentFromChildFragmentManager(currentPosition))
@ -245,7 +247,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
crossfadeViewIn(binding.mediaPreviewDetailsContainer)
}
private fun bindTextViews(currentItem: MediaTable.MediaRecord, showThread: Boolean) {
private fun bindTextViews(currentItem: MediaTable.MediaRecord, showThread: Boolean, messageBodies: Map<Long, SpannableString>) {
binding.toolbar.title = getTitleText(currentItem, showThread)
binding.toolbar.subtitle = getSubTitleText(currentItem)
val messageId: Long? = currentItem.attachment?.mmsId
@ -262,8 +264,27 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
}
val caption = currentItem.attachment?.caption
binding.mediaPreviewCaption.text = caption
binding.mediaPreviewCaption.visible = caption != null
if (caption != null) {
bindCaptionView(caption)
} else {
bindCaptionView(messageBodies[messageId])
}
}
private fun bindCaptionView(displayBody: CharSequence?) {
val caption: ExpandingCaptionView = binding.mediaPreviewCaption
if (displayBody.isNullOrEmpty()) {
caption.visible = false
} else {
caption.expandedHeight = calculateExpandedHeight()
caption.fullCaptionText = displayBody
caption.visible = true
}
}
private fun calculateExpandedHeight(): Int {
val height: Int = view?.height ?: return ViewUtil.dpToPx(requireContext(), EXPANDED_CAPTION_HEIGHT_FALLBACK_DP)
return ((height - binding.toolbar.height - binding.mediaPreviewPlaybackControls.height) * EXPANDED_CAPTION_HEIGHT_PERCENT).roundToInt()
}
private fun bindMenuItems(currentItem: MediaTable.MediaRecord) {
@ -602,6 +623,9 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
}
companion object {
private const val EXPANDED_CAPTION_HEIGHT_FALLBACK_DP = 400
private const val EXPANDED_CAPTION_HEIGHT_PERCENT: Float = 0.7F
private val TAG = Log.tag(MediaPreviewV2Fragment::class.java)
const val ARGS_KEY: String = "args"

Wyświetl plik

@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.mediapreview
import android.text.SpannableString
import org.thoughtcrime.securesms.database.MediaTable
import org.thoughtcrime.securesms.mediasend.Media
@ -11,6 +12,7 @@ data class MediaPreviewV2State(
val allMediaInAlbumRail: Boolean = false,
val leftIsRecent: Boolean = false,
val albums: Map<Long, List<Media>> = mapOf(),
val messageBodies: Map<Long, SpannableString> = mapOf(),
) {
enum class LoadState { INIT, DATA_LOADED, MEDIA_READY }
}

Wyświetl plik

@ -27,9 +27,9 @@ class MediaPreviewV2ViewModel : ViewModel() {
val currentPosition: Int
get() = store.state.position
fun fetchAttachments(startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaTable.Sorting, forceRefresh: Boolean = false) {
fun fetchAttachments(context: Context, startingAttachmentId: AttachmentId, threadId: Long, sorting: MediaTable.Sorting, forceRefresh: Boolean = false) {
if (store.state.loadState == MediaPreviewV2State.LoadState.INIT || forceRefresh) {
disposables += store.update(repository.getAttachments(startingAttachmentId, threadId, sorting)) { result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->
disposables += store.update(repository.getAttachments(context, startingAttachmentId, threadId, sorting)) { result: MediaPreviewRepository.Result, oldState: MediaPreviewV2State ->
val albums = result.records.fold(mutableMapOf()) { acc: MutableMap<Long, MutableList<Media>>, mediaRecord: MediaTable.MediaRecord ->
val attachment = mediaRecord.attachment
if (attachment != null) {
@ -42,6 +42,7 @@ class MediaPreviewV2ViewModel : ViewModel() {
oldState.copy(
position = result.initialPosition,
mediaRecords = result.records,
messageBodies = result.messageBodies,
albums = albums,
loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
)
@ -49,6 +50,7 @@ class MediaPreviewV2ViewModel : ViewModel() {
oldState.copy(
position = result.records.size - result.initialPosition - 1,
mediaRecords = result.records.reversed(),
messageBodies = result.messageBodies,
albums = albums.mapValues { it.value.reversed() },
loadState = MediaPreviewV2State.LoadState.DATA_LOADED,
)

Wyświetl plik

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.mediapreview.caption
import android.content.Context
import android.text.method.ScrollingMovementMethod
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
class ExpandingCaptionView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : EmojiTextView(context, attrs, defStyleAttr) {
var expandedHeight = 0
private var expanded: Boolean = false
var fullCaptionText: CharSequence = ""
set(value) {
field = value
expanded = false
updateExpansionState()
}
init {
setOnClickListener { toggleExpansion() }
}
fun toggleExpansion() {
expanded = !expanded
updateExpansionState()
}
private fun updateExpansionState() {
if (expanded) {
Log.d(TAG, "The view should be expanded now.")
text = fullCaptionText
movementMethod = ScrollingMovementMethod()
scrollTo(0, 0)
updateLayoutParams { height = expandedHeight }
} else {
Log.d(TAG, "The view should be collapsed now.")
text = if (fullCaptionText.length <= CHAR_LIMIT_MESSAGE_PREVIEW) {
fullCaptionText
} else {
context.getString(R.string.MediaPreviewFragment_see_more, fullCaptionText.substring(0, CHAR_LIMIT_MESSAGE_PREVIEW))
}
movementMethod = null
updateLayoutParams { height = ViewGroup.LayoutParams.WRAP_CONTENT }
}
setOnClickListener { toggleExpansion() }
}
companion object {
private val TAG = Log.tag(ExpandingCaptionView::class.java)
const val CHAR_LIMIT_MESSAGE_PREVIEW = 280
}
}

Wyświetl plik

@ -27,7 +27,7 @@
android:visibility="invisible"
tools:visibility="visible">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
<org.thoughtcrime.securesms.mediapreview.caption.ExpandingCaptionView
android:id="@+id/media_preview_caption"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"

Wyświetl plik

@ -2010,6 +2010,8 @@
<string name="MediaPreviewFragment_edit_media_error">Media Error</string>
<!-- This is displayed as a toast notification when we encounter an error deleting a message, including potentially on other people\'s devices -->
<string name="MediaPreviewFragment_media_delete_error">Error Deleting Message, Message May Still Exist</string>
<!-- A suffix to be attached to truncated captions that the user may tap onto to view the entire text caption -->
<string name="MediaPreviewFragment_see_more">%1$s… <b>Read More</b></string>
<!-- MessageNotifier -->
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>