Update MediaPreviewV2 to use thumbnail rail & menu items.

fork-5.53.8
Nicholas 2022-10-05 16:10:28 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 2edb9eeb52
commit a9a64a3f60
7 zmienionych plików z 361 dodań i 26 usunięć

Wyświetl plik

@ -1,13 +1,22 @@
package org.thoughtcrime.securesms.mediapreview package org.thoughtcrime.securesms.mediapreview
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.commit import androidx.fragment.app.commit
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) { class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) {
override fun attachBaseContext(newBase: Context) {
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setTheme(R.style.TextSecure_MediaPreview)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val bundle = Bundle() val bundle = Bundle()
val args = MediaIntentFactory.requireArguments(intent.extras!!) val args = MediaIntentFactory.requireArguments(intent.extras!!)

Wyświetl plik

@ -1,23 +1,52 @@
package org.thoughtcrime.securesms.mediapreview package org.thoughtcrime.securesms.mediapreview
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.Toast
import androidx.core.app.ShareCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.animation.DepthPageTransformer import org.thoughtcrime.securesms.animation.DepthPageTransformer
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.FullscreenHelper
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.SaveAttachmentTask
import org.thoughtcrime.securesms.util.StorageUtil
import java.util.Locale import java.util.Locale
import java.util.Optional
class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events { class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events {
private val TAG = Log.tag(MediaPreviewV2Fragment::class.java) private val TAG = Log.tag(MediaPreviewV2Fragment::class.java)
@ -35,7 +64,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
initializeViewModel()
val args = MediaIntentFactory.requireArguments(requireArguments())
initializeViewModel(args)
initializeToolbar(binding.toolbar, args)
binding.mediaPager.offscreenPageLimit = 1 binding.mediaPager.offscreenPageLimit = 1
binding.mediaPager.setPageTransformer(DepthPageTransformer()) binding.mediaPager.setPageTransformer(DepthPageTransformer())
val adapter = MediaPreviewV2Adapter(this) val adapter = MediaPreviewV2Adapter(this)
@ -47,18 +80,59 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
} }
}) })
initializeFullScreenUi() initializeFullScreenUi()
initializeAlbumRail()
anchorMarginsToBottomInsets(binding.mediaPreviewDetailsContainer)
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe { lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
bindCurrentState(it) bindCurrentState(it)
} }
} }
private fun initializeFullScreenUi() { private fun initializeToolbar(toolbar: MaterialToolbar, args: MediaIntentFactory.MediaPreviewArgs) {
fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar) toolbar.setNavigationOnClickListener {
fullscreenHelper.hideSystemUI() requireActivity().onBackPressed()
}
binding.toolbar.inflateMenu(R.menu.media_preview)
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
binding.toolbar.menu.findItem(R.id.media_preview__share).isVisible = Build.VERSION.SDK_INT >= 26
if (args.hideAllMedia) {
binding.toolbar.menu.findItem(R.id.media_preview__overview).isVisible = false
}
} }
private fun initializeViewModel() { private fun initializeAlbumRail() {
val args = MediaIntentFactory.requireArguments(requireArguments()) binding.mediaPreviewAlbumRail.itemAnimator = null // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
binding.mediaPreviewAlbumRail.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.mediaPreviewAlbumRail.adapter = MediaRailAdapter(
GlideApp.with(this),
object : MediaRailAdapter.RailItemListener {
override fun onRailItemClicked(distanceFromActive: Int) {
binding.mediaPager.currentItem += distanceFromActive
}
override fun onRailItemDeleteClicked(distanceFromActive: Int) {
throw UnsupportedOperationException("Callback unsupported.")
}
},
false
)
}
private fun initializeFullScreenUi() {
fullscreenHelper.configureToolbarLayout(binding.toolbarCutoutSpacer, binding.toolbar)
fullscreenHelper.showAndHideWithSystemUI(requireActivity().window, binding.toolbarLayout, binding.mediaPreviewDetailsContainer)
}
private fun initializeViewModel(args: MediaIntentFactory.MediaPreviewArgs) {
if (!MediaUtil.isImageType(args.initialMediaType) && !MediaUtil.isVideoType(args.initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewV2Fragment, finishing.")
Snackbar.make(binding.root, R.string.MediaPreviewActivity_unssuported_media_type, Snackbar.LENGTH_LONG)
.setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) {
requireActivity().finish()
}.show()
}
viewModel.setShowThread(args.showThread) viewModel.setShowThread(args.showThread)
val sorting = MediaDatabase.Sorting.values()[args.sorting] val sorting = MediaDatabase.Sorting.values()[args.sorting]
viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting) viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting)
@ -67,7 +141,11 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
private fun bindCurrentState(currentState: MediaPreviewV2State) { private fun bindCurrentState(currentState: MediaPreviewV2State) {
when (currentState.loadState) { when (currentState.loadState) {
MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState) MediaPreviewV2State.LoadState.READY -> bindReadyState(currentState)
// INIT, else -> no-op MediaPreviewV2State.LoadState.LOADED -> {
bindReadyState(currentState)
bindLoadedState(currentState)
}
else -> null
} }
} }
@ -76,6 +154,58 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position] val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
binding.toolbar.title = getTitleText(currentItem, currentState.showThread) binding.toolbar.title = getTitleText(currentItem, currentState.showThread)
binding.toolbar.subtitle = getSubTitleText(currentItem) binding.toolbar.subtitle = getSubTitleText(currentItem)
val menu: Menu = binding.toolbar.menu
if (currentItem.threadId == MediaIntentFactory.NOT_IN_A_THREAD.toLong()) {
menu.findItem(R.id.media_preview__overview).isVisible = false
menu.findItem(R.id.delete).isVisible = false
}
binding.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.media_preview__overview -> showOverview(currentItem.threadId)
R.id.media_preview__forward -> forward(currentItem)
R.id.media_preview__share -> share(currentItem)
R.id.save -> saveToDisk(currentItem)
R.id.delete -> deleteMedia(currentItem)
android.R.id.home -> requireActivity().finish()
else -> return@setOnMenuItemClickListener false
}
return@setOnMenuItemClickListener true
}
}
/**
* These are binding steps that need a reference to the actual fragment within the pager.
* This is not available until after a page has been chosen by the ViewPager, and we receive the
* {@link OnPageChangeCallback}.
*/
private fun bindLoadedState(currentState: MediaPreviewV2State) {
val currentItem: MediaDatabase.MediaRecord = currentState.mediaRecords[currentState.position]
val currentFragment: Fragment? = childFragmentManager.findFragmentByTag("f${currentState.position}")
val playbackControls = (currentFragment as? MediaPreviewFragment)?.playbackControls
val albumThumbnailMedia = currentState.mediaRecords.map { it.toMedia() }
val caption = currentItem.attachment?.caption
if (albumThumbnailMedia.isEmpty() && caption == null && playbackControls == null) {
binding.mediaPreviewDetailsContainer.visibility = View.GONE
} else {
binding.mediaPreviewDetailsContainer.visibility = View.VISIBLE
}
binding.mediaPreviewAlbumRail.visibility = if (albumThumbnailMedia.isEmpty()) View.GONE else View.VISIBLE
(binding.mediaPreviewAlbumRail.adapter as MediaRailAdapter).setMedia(albumThumbnailMedia, currentState.position)
binding.mediaPreviewAlbumRail.smoothScrollToPosition(currentState.position)
binding.mediaPreviewCaptionContainer.visibility = if (caption == null) View.GONE else View.VISIBLE
binding.mediaPreviewCaption.text = caption
if (playbackControls != null) {
val params = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
playbackControls.layoutParams = params
binding.mediaPreviewPlaybackControlsContainer.removeAllViews()
binding.mediaPreviewPlaybackControlsContainer.addView(playbackControls)
} else {
binding.mediaPreviewPlaybackControlsContainer.removeAllViews()
}
} }
private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String { private fun getTitleText(mediaRecord: MediaDatabase.MediaRecord, showThread: Boolean): String {
@ -112,20 +242,148 @@ class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), Med
getString(R.string.MediaPreviewActivity_draft) getString(R.string.MediaPreviewActivity_draft)
} }
private fun anchorMarginsToBottomInsets(viewToAnchor: View) {
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor) { view: View, windowInsetsCompat: WindowInsetsCompat ->
val layoutParams = view.layoutParams as MarginLayoutParams
val systemBarInsets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars())
layoutParams.setMargins(
systemBarInsets.left,
layoutParams.topMargin,
systemBarInsets.right,
systemBarInsets.bottom
)
view.layoutParams = layoutParams
windowInsetsCompat
}
}
private fun MediaDatabase.MediaRecord.toMedia(): Media? {
val attachment = this.attachment
val uri = attachment?.uri
if (attachment == null || uri == null) {
return null
}
return Media(
uri,
this.contentType,
this.date,
attachment.width,
attachment.height,
attachment.size,
0,
attachment.isBorderless,
attachment.isVideoGif,
Optional.empty(),
Optional.ofNullable(attachment.caption),
Optional.empty()
)
}
override fun singleTapOnMedia(): Boolean { override fun singleTapOnMedia(): Boolean {
Log.d(TAG, "singleTapOnMedia()") fullscreenHelper.toggleUiVisibility()
return true return true
} }
override fun mediaNotAvailable() { override fun mediaNotAvailable() {
Log.d(TAG, "mediaNotAvailable()") Snackbar.make(binding.root, R.string.MediaPreviewActivity_media_no_longer_available, Snackbar.LENGTH_LONG)
.setAction(R.string.MediaPreviewActivity_dismiss_due_to_error) {
requireActivity().finish()
}.show()
} }
override fun onMediaReady() { override fun onMediaReady() {
Log.d(TAG, "onMediaReady()") Log.d(TAG, "onMediaReady()")
} }
private fun showOverview(threadId: Long) {
val context = requireContext()
context.startActivity(MediaOverviewActivity.forThread(context, threadId))
}
private fun forward(mediaItem: MediaDatabase.MediaRecord) {
val attachment = mediaItem.attachment
val uri = attachment?.uri
if (attachment != null && uri != null) {
MultiselectForwardFragmentArgs.create(
requireContext(),
mediaItem.threadId,
uri,
attachment.contentType
) { args: MultiselectForwardFragmentArgs ->
MultiselectForwardFragment.showBottomSheet(childFragmentManager, args)
}
}
}
private fun share(mediaItem: MediaDatabase.MediaRecord) {
val attachment = mediaItem.attachment
val uri = attachment?.uri
if (attachment != null && uri != null) {
val publicUri = PartAuthority.getAttachmentPublicUri(uri)
val mimeType = Intent.normalizeMimeType(attachment.contentType)
val shareIntent = ShareCompat.IntentBuilder(requireActivity())
.setStream(publicUri)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
startActivity(shareIntent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No activity existed to share the media.", e)
Toast.makeText(requireContext(), R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show()
}
}
}
private fun saveToDisk(mediaItem: MediaDatabase.MediaRecord) {
SaveAttachmentTask.showWarningDialog(requireContext()) { _: DialogInterface?, _: Int ->
if (StorageUtil.canWriteToMediaStore()) {
performSaveToDisk(mediaItem)
return@showWarningDialog
}
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.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show() }
.onAllGranted { performSaveToDisk(mediaItem) }
.execute()
}
}
fun performSaveToDisk(mediaItem: MediaDatabase.MediaRecord) {
val saveTask = SaveAttachmentTask(requireContext())
val saveDate = if (mediaItem.date > 0) mediaItem.date else System.currentTimeMillis()
val attachment = mediaItem.attachment
val uri = attachment?.uri
if (attachment != null && uri != null) {
saveTask.executeOnExecutor(SignalExecutors.BOUNDED_IO, SaveAttachmentTask.Attachment(uri, attachment.contentType, saveDate, null))
}
}
private fun deleteMedia(mediaItem: MediaDatabase.MediaRecord) {
val attachment: DatabaseAttachment = mediaItem.attachment ?: return
MaterialAlertDialogBuilder(requireContext())
.setIcon(R.drawable.ic_warning)
.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title)
.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message)
.setCancelable(true)
.setPositiveButton(R.string.delete) { _, _ ->
viewModel.deleteItem(requireContext(), attachment, onSuccess = {
requireActivity().finish()
}, onError = {
Log.e(TAG, "Delete failed!", it)
requireActivity().finish()
})
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
companion object { companion object {
val ARGS_KEY: String = "args" const val ARGS_KEY: String = "args"
} }
} }

Wyświetl plik

@ -8,5 +8,5 @@ data class MediaPreviewV2State(
val position: Int = 0, val position: Int = 0,
val showThread: Boolean = false val showThread: Boolean = false
) { ) {
enum class LoadState { INIT, READY, } enum class LoadState { INIT, READY, LOADED }
} }

Wyświetl plik

@ -1,13 +1,19 @@
package org.thoughtcrime.securesms.mediapreview package org.thoughtcrime.securesms.mediapreview
import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.kotlin.plusAssign import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.MediaDatabase import org.thoughtcrime.securesms.database.MediaDatabase
import org.thoughtcrime.securesms.util.AttachmentUtil
import org.thoughtcrime.securesms.util.rx.RxStore import org.thoughtcrime.securesms.util.rx.RxStore
class MediaPreviewV2ViewModel : ViewModel() { class MediaPreviewV2ViewModel : ViewModel() {
@ -19,7 +25,8 @@ class MediaPreviewV2ViewModel : ViewModel() {
val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) { fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) {
disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { mediaRecords: List<MediaDatabase.MediaRecord>, oldState: MediaPreviewV2State -> disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) {
mediaRecords: List<MediaDatabase.MediaRecord>, oldState: MediaPreviewV2State ->
oldState.copy( oldState.copy(
mediaRecords = mediaRecords, mediaRecords = mediaRecords,
loadState = MediaPreviewV2State.LoadState.READY loadState = MediaPreviewV2State.LoadState.READY
@ -35,10 +42,17 @@ class MediaPreviewV2ViewModel : ViewModel() {
fun setCurrentPage(position: Int) { fun setCurrentPage(position: Int) {
store.update { oldState -> store.update { oldState ->
oldState.copy(position = position) oldState.copy(position = position, loadState = MediaPreviewV2State.LoadState.LOADED)
} }
} }
fun deleteItem(context: Context, attachment: DatabaseAttachment, onSuccess: Consumer<in Unit>, onError: Consumer<in Throwable>) {
disposables += Single.fromCallable { AttachmentUtil.deleteAttachment(context.applicationContext, attachment) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError)
}
override fun onCleared() { override fun onCleared() {
disposables.dispose() disposables.dispose()
store.dispose() store.dispose()

Wyświetl plik

@ -3,4 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view" android:id="@+id/fragment_container_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:background="@color/signal_dark_colorNeutral"/>

Wyświetl plik

@ -1,11 +1,67 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/core_grey_95" xmlns:tools="http://schemas.android.com/tools"
android:theme="@style/TextSecure.MediaPreview"> android:background="@color/signal_dark_colorNeutral">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/media_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true" />
<LinearLayout
android:id="@+id/media_preview_details_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:animateLayoutChanges="true"
android:background="@drawable/image_preview_shade"
android:gravity="bottom"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<org.thoughtcrime.securesms.components.MaxHeightScrollView
android:id="@+id/media_preview_caption_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="32dp"
android:animateLayoutChanges="true"
app:scrollView_maxHeight="120dp">
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/media_preview_caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
style="@style/Signal.Text.Body"
android:textColor="@color/core_white"
android:gravity="bottom"
tools:text="With great power comes great responsibility." />
</org.thoughtcrime.securesms.components.MaxHeightScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_preview_album_rail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:layout_height="64dp"/>
<FrameLayout
android:id="@+id/media_preview_playback_controls_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"/>
</LinearLayout>
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout" android:id="@+id/toolbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -19,18 +75,14 @@
android:layout_height="0dp" android:layout_height="0dp"
android:visibility="gone" /> android:visibility="gone" />
<androidx.appcompat.widget.Toolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:theme="?actionBarStyle" android:theme="?actionBarStyle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:background="@android:color/transparent" /> android:background="@android:color/transparent"
app:navigationIcon="@drawable/ic_arrow_left_white_24" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/media_pager" </androidx.coordinatorlayout.widget.CoordinatorLayout>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true" />
</FrameLayout>

Wyświetl plik

@ -1965,6 +1965,7 @@
<string name="MediaPreviewActivity_s_to_you">%1$s to you</string> <string name="MediaPreviewActivity_s_to_you">%1$s to you</string>
<string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string> <string name="MediaPreviewActivity_media_no_longer_available">Media no longer available.</string>
<string name="MediaPreviewActivity_cant_find_an_app_able_to_share_this_media">Can\'t find an app able to share this media.</string> <string name="MediaPreviewActivity_cant_find_an_app_able_to_share_this_media">Can\'t find an app able to share this media.</string>
<string name="MediaPreviewActivity_dismiss_due_to_error">Close</string>
<!-- MessageNotifier --> <!-- MessageNotifier -->
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string> <string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>