Uploads: add screen - WIP

This commit is contained in:
Benoit Marty 2020-05-19 17:39:10 +02:00
parent 0992e76800
commit e9ca876444
28 changed files with 868 additions and 109 deletions

View File

@ -220,3 +220,20 @@ fun Event.isImageMessage(): Boolean {
else -> false else -> false
} }
} }
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}
fun Event.isPreviewableMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session.room.uploads
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
data class GetUploadsResult( data class GetUploadsResult(
// List of fetched Events // List of fetched Events, most recent first
val events: List<Event>, val events: List<Event>,
// token to get more events, or null if there is no more event to fetch // token to get more events, or null if there is no more event to fetch
val nextToken: String? val nextToken: String?

View File

@ -38,7 +38,9 @@ internal class DefaultUploadsService @AssistedInject constructor(
override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback<GetUploadsResult>): Cancelable { override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback<GetUploadsResult>): Cancelable {
return getUploadsTask return getUploadsTask
.configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) .configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) {
this.callback = callback
}
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }

View File

@ -51,7 +51,7 @@ internal class DefaultGetUploadsTask @Inject constructor(
} }
return GetUploadsResult( return GetUploadsResult(
events = chunk.events, // reverse? events = chunk.events,
nextToken = chunk.end?.takeIf { it != chunk.start } nextToken = chunk.end?.takeIf { it != chunk.start }
) )
} }

View File

@ -77,8 +77,8 @@ import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment import im.vector.riotx.features.settings.VectorSettingsLabsFragment

View File

@ -14,20 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.roomprofile.uploads.child package im.vector.riotx.core.epoxy
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import javax.inject.Inject
/** @EpoxyModelClass(layout = R.layout.item_loading_square)
* A placeholder fragment containing a simple view. abstract class SquareLoadingItem : VectorEpoxyModel<SquareLoadingItem.Holder>() {
*/
class RoomUploadsFilesFragment @Inject constructor() : VectorBaseFragment() {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class) class Holder : VectorEpoxyHolder()
override fun getLayoutResId() = R.layout.fragment_generic_recycler
} }

View File

@ -36,4 +36,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
resources.displayMetrics resources.displayMetrics
).toInt() ).toInt()
} }
fun pdToDp(px: Int): Int {
return (px.toFloat() / resources.displayMetrics.density).toInt()
}
} }

View File

@ -22,7 +22,6 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Spannable import android.text.Spannable
@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.Window
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
@ -150,7 +147,6 @@ import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -998,31 +994,14 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
requireActivity().window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
requireActivity().window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
}
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: "")) pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: "")) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), *pairs.toTypedArray()).toBundle()
startActivity(intent, bundle)
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openVideoViewer(requireActivity(), mediaData)
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
} }
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

View File

@ -65,6 +65,17 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
STICKER STICKER
} }
/**
* For gallery
*/
fun render(data: Data, imageView: ImageView, size: Int) {
// a11y
imageView.contentDescription = data.filename
createGlideRequest(data, Mode.THUMBNAIL, imageView, Size(size, size))
.into(imageView)
}
fun render(data: Data, mode: Mode, imageView: ImageView) { fun render(data: Data, mode: Mode, imageView: ImageView) {
val size = processSize(data, mode) val size = processSize(data, mode)
imageView.layoutParams.width = size.width imageView.layoutParams.width = size.width

View File

@ -19,9 +19,12 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.view.View import android.view.View
import android.view.Window
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.TaskStackBuilder import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
@ -45,6 +48,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.BigImageViewerActivity import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -215,6 +222,29 @@ class DefaultNavigator @Inject constructor(
fragment.startActivityForResult(intent, requestCode) fragment.startActivityForResult(intent, requestCode)
} }
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
}
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
options?.invoke(pairs)
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
activity.startActivity(intent, bundle)
}
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
activity.startActivity(intent)
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) { private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
if (buildTask) { if (buildTask) {
val stackBuilder = TaskStackBuilder.create(context) val stackBuilder = TaskStackBuilder.create(context)

View File

@ -19,10 +19,13 @@ package im.vector.riotx.features.navigation
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.core.util.Pair
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.terms.TermsService import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.terms.ReviewTermsActivity import im.vector.riotx.features.terms.ReviewTermsActivity
@ -76,4 +79,8 @@ interface Navigator {
baseUrl: String, baseUrl: String,
token: String?, token: String?,
requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE) requestCode: Int = ReviewTermsActivity.TERMS_REQUEST_CODE)
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
} }

View File

@ -16,6 +16,12 @@
package im.vector.riotx.features.roomprofile.uploads package im.vector.riotx.features.roomprofile.uploads
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.core.platform.VectorViewModelAction import im.vector.riotx.core.platform.VectorViewModelAction
sealed class RoomUploadsAction : VectorViewModelAction sealed class RoomUploadsAction : VectorViewModelAction {
data class Download(val event: Event) : RoomUploadsAction()
data class Share(val event: Event) : RoomUploadsAction()
object Retry : RoomUploadsAction()
}

View File

@ -21,6 +21,7 @@ import android.view.View
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.tabs.TabLayoutMediator
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
@ -45,11 +46,17 @@ class RoomUploadsFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val sectionsPagerAdapter = RoomUploadsPagerAdapter(childFragmentManager, stringProvider) val sectionsPagerAdapter = RoomUploadsPagerAdapter(this)
view_pager.adapter = sectionsPagerAdapter roomUploadsViewPager.adapter = sectionsPagerAdapter
tabs.setupWithViewPager(view_pager)
setupToolbar(matrixProfileToolbar) TabLayoutMediator(roomUploadsTabs, roomUploadsViewPager) { tab, position ->
when (position) {
0 -> tab.text = stringProvider.getString(R.string.uploads_media_title)
1 -> tab.text = stringProvider.getString(R.string.uploads_files_title)
}
}.attach()
setupToolbar(roomUploadsToolbar)
// Initialize your view, subscribe to viewModel... // Initialize your view, subscribe to viewModel...
} }
@ -68,8 +75,8 @@ class RoomUploadsFragment @Inject constructor(
private fun renderRoomSummary(state: RoomUploadsViewState) { private fun renderRoomSummary(state: RoomUploadsViewState) {
state.roomSummary()?.let { state.roomSummary()?.let {
matrixProfileToolbarTitleView.text = it.displayName roomUploadsToolbarTitleView.text = it.displayName
avatarRenderer.render(it.toMatrixItem(), matrixProfileToolbarAvatarImageView) avatarRenderer.render(it.toMatrixItem(), roomUploadsToolbarAvatarImageView)
} }
} }
} }

View File

@ -17,43 +17,21 @@
package im.vector.riotx.features.roomprofile.uploads package im.vector.riotx.features.roomprofile.uploads
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.fragment.app.FragmentPagerAdapter import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.R import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.child.RoomUploadsMediaFragment
private val TAB_TITLES = arrayOf(
R.string.uploads_title_media,
R.string.uploads_title_files
)
/**
* A [FragmentPagerAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
class RoomUploadsPagerAdapter( class RoomUploadsPagerAdapter(
fm: FragmentManager, private val fragment: Fragment
private val stringProvider: StringProvider ) : FragmentStateAdapter(fragment) {
) : FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItem(position: Int): Fragment { override fun getItemCount() = 2
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below). override fun createFragment(position: Int): Fragment {
return if (position == 0) { return if (position == 0) {
RoomUploadsMediaFragment() fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsMediaFragment::class.java.name)
} else { } else {
RoomUploadsFilesFragment() fragment.childFragmentManager.fragmentFactory.instantiate(fragment.requireContext().classLoader, RoomUploadsFilesFragment::class.java.name)
} }
} }
override fun getPageTitle(position: Int): CharSequence? {
return stringProvider.getString(TAB_TITLES[position])
}
override fun getCount(): Int {
// Show 2 total pages.
return 2
}
} }

View File

@ -27,6 +27,10 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isPreviewableMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
@ -90,10 +94,15 @@ class RoomUploadsViewModel @AssistedInject constructor(
token = result.nextToken token = result.nextToken
val groupedEvents = result.events
.filter { it.getClearType() == EventType.MESSAGE && it.getClearContent()?.toModel<MessageContent>() != null }
.groupBy { it.isPreviewableMessage() }
setState { setState {
copy( copy(
asyncEventsRequest = Uninitialized, asyncEventsRequest = Uninitialized,
events = this.events + result.events, mediaEvents = this.mediaEvents + groupedEvents[true].orEmpty(),
fileEvents = this.fileEvents + groupedEvents[false].orEmpty(),
hasMore = result.nextToken != null hasMore = result.nextToken != null
) )
} }

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.files
import android.os.Bundle
import android.view.View
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class RoomUploadsFilesFragment @Inject constructor(
private val controller: UploadsFileController
) : VectorBaseFragment(), UploadsFileController.Listener {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(controller, showDivider = true)
controller.listener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
controller.listener = null
}
override fun onOpenClicked(event: Event) {
// TODO
}
override fun onRetry() {
uploadsViewModel.handle(RoomUploadsAction.Retry)
}
override fun onDownloadClicked(event: Event) {
uploadsViewModel.handle(RoomUploadsAction.Download(event))
}
override fun onShareClicked(event: Event) {
uploadsViewModel.handle(RoomUploadsAction.Share(event))
}
override fun invalidate() = withState(uploadsViewModel) { state ->
controller.setData(state)
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.files
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
import javax.inject.Inject
class UploadsFileController @Inject constructor(
private val errorFormatter: ErrorFormatter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomUploadsViewState>() {
interface Listener {
fun onRetry()
fun onOpenClicked(event: Event)
fun onDownloadClicked(event: Event)
fun onShareClicked(event: Event)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var listener: Listener? = null
init {
setData(null)
}
override fun buildModels(data: RoomUploadsViewState?) {
data ?: return
if (data.fileEvents.isEmpty()) {
when (data.asyncEventsRequest) {
is Loading -> {
loadingItem {
id("loading")
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error))
listener { listener?.onRetry() }
}
}
}
} else {
buildFileItems(data.fileEvents)
if (data.hasMore) {
loadingItem {
id("loadMore")
}
}
}
}
private fun buildFileItems(fileEvents: List<Event>) {
fileEvents.forEach {
uploadsFileItem {
id(it.eventId ?: "")
title(it.getClearType())
subtitle(it.getSenderKey())
listener(object : UploadsFileItem.Listener {
override fun onItemClicked() {
listener?.onOpenClicked(it)
}
override fun onDownloadClicked() {
listener?.onDownloadClicked(it)
}
override fun onShareClicked() {
listener?.onShareClicked(it)
}
})
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.files
import android.view.View
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.extensions.setTextOrHide
@EpoxyModelClass(layout = R.layout.item_uploads_file)
abstract class UploadsFileItem : VectorEpoxyModel<UploadsFileItem.Holder>() {
@EpoxyAttribute var title: String? = null
@EpoxyAttribute var subtitle: String? = null
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked() }
holder.titleView.text = title
holder.subtitleView.setTextOrHide(subtitle)
holder.downloadView.setOnClickListener { listener?.onDownloadClicked() }
holder.shareView.setOnClickListener { listener?.onShareClicked() }
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.uploadsFileTitle)
val subtitleView by bind<TextView>(R.id.uploadsFileSubtitle)
val downloadView by bind<View>(R.id.uploadsFileActionDownload)
val shareView by bind<View>(R.id.uploadsFileActionShare)
}
interface Listener {
fun onItemClicked()
fun onDownloadClicked()
fun onShareClicked()
}
}

View File

@ -14,20 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.roomprofile.uploads.child package im.vector.riotx.features.roomprofile.uploads.media
import com.airbnb.mvrx.parentFragmentViewModel // Min image size. Size will be adjusted at runtime
import im.vector.riotx.R const val IMAGE_SIZE_DP = 120
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import javax.inject.Inject
/**
* A placeholder fragment containing a simple view.
*/
class RoomUploadsMediaFragment @Inject constructor() : VectorBaseFragment() {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.media
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class RoomUploadsMediaFragment @Inject constructor(
private val controller: UploadsMediaController,
private val dimensionConverter: DimensionConverter
) : VectorBaseFragment(), UploadsMediaController.Listener {
private val uploadsViewModel by parentFragmentViewModel(RoomUploadsViewModel::class)
override fun getLayoutResId() = R.layout.fragment_generic_recycler
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.layoutManager = GridLayoutManager(context, getNumberOfColumns())
recyclerView.adapter = controller.adapter
recyclerView.setHasFixedSize(true)
controller.listener = this
}
private fun getNumberOfColumns(): Int {
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
return dimensionConverter.pdToDp(displayMetrics.widthPixels) / IMAGE_SIZE_DP
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
controller.listener = null
}
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
navigator.openImageViewer(requireActivity(), mediaData, view, null)
}
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
navigator.openVideoViewer(requireActivity(), mediaData)
}
override fun onRetry() {
uploadsViewModel.handle(RoomUploadsAction.Retry)
}
override fun invalidate() = withState(uploadsViewModel) { state ->
controller.setData(state)
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_image)
abstract class UploadsImageItem : VectorEpoxyModel<UploadsImageItem.Holder>() {
@EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer
@EpoxyAttribute lateinit var data: ImageContentRenderer.Data
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
}
class Holder : VectorEpoxyHolder() {
val imageView by bind<ImageView>(R.id.uploadsImagePreview)
}
interface Listener {
fun onItemClicked(view: View, data: ImageContentRenderer.Data)
}
}

View File

@ -0,0 +1,165 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isVideoMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.squareLoadingItem
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
import javax.inject.Inject
class UploadsMediaController @Inject constructor(
private val errorFormatter: ErrorFormatter,
private val imageContentRenderer: ImageContentRenderer,
private val dimensionConverter: DimensionConverter,
colorProvider: ColorProvider
) : TypedEpoxyController<RoomUploadsViewState>() {
interface Listener {
fun onRetry()
fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data)
fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data)
}
private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color)
var listener: Listener? = null
private val itemSize = dimensionConverter.dpToPx(64)
init {
setData(null)
}
override fun buildModels(data: RoomUploadsViewState?) {
data ?: return
if (data.mediaEvents.isEmpty()) {
when (data.asyncEventsRequest) {
is Loading -> {
squareLoadingItem {
id("loading")
}
}
is Fail -> {
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(data.asyncEventsRequest.error))
listener { listener?.onRetry() }
}
}
}
} else {
buildMediaItems(data.mediaEvents)
if (data.hasMore) {
squareLoadingItem {
id("loadMore")
}
}
}
}
private fun buildMediaItems(mediaEvents: List<Event>) {
mediaEvents.forEach { event ->
when {
event.isImageMessage() -> {
val data = event.toImageContentRendererData() ?: return@forEach
uploadsImageItem {
id(event.eventId ?: "")
imageContentRenderer(imageContentRenderer)
data(data)
listener(object : UploadsImageItem.Listener {
override fun onItemClicked(view: View, data: ImageContentRenderer.Data) {
listener?.onOpenImageClicked(view, data)
}
})
}
}
event.isVideoMessage() -> {
val data = event.toVideoContentRendererData() ?: return@forEach
uploadsVideoItem {
id(event.eventId ?: "")
imageContentRenderer(imageContentRenderer)
data(data)
listener(object : UploadsVideoItem.Listener {
override fun onItemClicked(view: View, data: VideoContentRenderer.Data) {
listener?.onOpenVideoClicked(view, data)
}
})
}
}
}
}
}
private fun Event.toImageContentRendererData(): ImageContentRenderer.Data? {
val messageContent = getClearContent()?.toModel<MessageImageContent>() ?: return null
return ImageContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
height = messageContent.info?.height,
maxHeight = itemSize,
width = messageContent.info?.width,
maxWidth = itemSize
)
}
private fun Event.toVideoContentRendererData(): VideoContentRenderer.Data? {
val messageContent = getClearContent()?.toModel<MessageVideoContent>() ?: return null
val thumbnailData = ImageContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = itemSize,
width = messageContent.videoInfo?.width,
maxWidth = itemSize
)
return VideoContentRenderer.Data(
eventId = eventId ?: "",
filename = messageContent.body,
url = messageContent.getFileUrl(),
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.roomprofile.uploads.media
import android.view.View
import android.widget.ImageView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@EpoxyModelClass(layout = R.layout.item_uploads_video)
abstract class UploadsVideoItem : VectorEpoxyModel<UploadsVideoItem.Holder>() {
@EpoxyAttribute lateinit var imageContentRenderer: ImageContentRenderer
@EpoxyAttribute lateinit var data: VideoContentRenderer.Data
@EpoxyAttribute var listener: Listener? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
}
class Holder : VectorEpoxyHolder() {
val imageView by bind<ImageView>(R.id.uploadsVideoPreview)
}
interface Listener {
fun onItemClicked(view: View, data: VideoContentRenderer.Data)
}
}

View File

@ -12,7 +12,7 @@
android:elevation="4dp"> android:elevation="4dp">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/matrixProfileToolbar" android:id="@+id/roomUploadsToolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"
@ -20,12 +20,12 @@
app:layout_collapseMode="pin"> app:layout_collapseMode="pin">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/matrixProfileToolbarContainer" android:id="@+id/roomUploadsToolbarContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<ImageView <ImageView
android:id="@+id/matrixProfileToolbarAvatarImageView" android:id="@+id/roomUploadsToolbarAvatarImageView"
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@ -36,17 +36,17 @@
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
<ImageView <ImageView
android:id="@+id/matrixProfileDecorationToolbarAvatarImageView" android:id="@+id/roomUploadsDecorationToolbarAvatarImageView"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
app:layout_constraintCircle="@+id/matrixProfileToolbarAvatarImageView" app:layout_constraintCircle="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintCircleAngle="135" app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="20dp" app:layout_constraintCircleRadius="20dp"
tools:ignore="MissingConstraints" tools:ignore="MissingConstraints"
tools:src="@drawable/ic_shield_trusted" /> tools:src="@drawable/ic_shield_trusted" />
<im.vector.riotx.core.platform.EllipsizingTextView <im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/matrixProfileToolbarTitleView" android:id="@+id/roomUploadsToolbarTitleView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
@ -57,7 +57,7 @@
android:textSize="18sp" android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/matrixProfileToolbarAvatarImageView" app:layout_constraintStart_toEndOf="@+id/roomUploadsToolbarAvatarImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/roomName" /> tools:text="@sample/matrix.json/data/roomName" />
@ -66,18 +66,18 @@
</androidx.appcompat.widget.Toolbar> </androidx.appcompat.widget.Toolbar>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/tabs" android:id="@+id/roomUploadsTabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/matrixProfileToolbarAvatarImageView" app:layout_constraintTop_toBottomOf="@+id/roomUploadsToolbarAvatarImageView"
app:tabGravity="fill" app:tabGravity="fill"
app:tabMaxWidth="0dp" app:tabMaxWidth="0dp"
app:tabMode="fixed" /> app:tabMode="fixed" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/roomUploadsViewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:minHeight="64dp">
<ImageView
android:id="@+id/uploadsFileIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:src="@drawable/ic_file"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/uploadsFileSubtitle"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionDownload"
app:layout_constraintStart_toEndOf="@+id/uploadsFileIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Filename.file" />
<im.vector.riotx.core.platform.EllipsizingTextView
android:id="@+id/uploadsFileSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/uploadsFileTitle"
app:layout_constraintStart_toStartOf="@+id/uploadsFileTitle"
app:layout_constraintTop_toBottomOf="@+id/uploadsFileTitle"
tools:text="Username at 12:00 on 01/01/01" />
<ImageView
android:id="@+id/uploadsFileActionDownload"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:scaleType="center"
android:src="@drawable/ic_download"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/uploadsFileActionShare"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/uploadsFileActionShare"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:scaleType="center"
android:src="@drawable/ic_material_share"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsImagePreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground">
<ImageView
android:id="@+id/uploadsVideoPreview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="2dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_material_play_circle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>