Adjust colors for avatar and display names + start handling video in timeline

This commit is contained in:
ganfra 2019-04-11 19:19:52 +02:00
parent c38a601bcc
commit 9c9c09db2b
21 changed files with 188 additions and 100 deletions

View File

@ -31,6 +31,7 @@ class AutocompleteUserController : TypedEpoxyController<List<User>>() {
data.forEach { user -> data.forEach { user ->
autocompleteUserItem { autocompleteUserItem {
id(user.userId) id(user.userId)
userId(user.userId)
name(user.displayName) name(user.displayName)
avatarUrl(user.avatarUrl) avatarUrl(user.avatarUrl)
clickListener { _ -> clickListener { _ ->

View File

@ -29,18 +29,15 @@ import im.vector.riotredesign.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_autocomplete_user) @EpoxyModelClass(layout = R.layout.item_autocomplete_user)
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() { abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute var name: String? = null
var name: String? = null @EpoxyAttribute var userId: String = ""
@EpoxyAttribute @EpoxyAttribute var avatarUrl: String? = null
var avatarUrl: String? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
holder.nameView.text = name holder.nameView.text = name
AvatarRenderer.render(avatarUrl, name, holder.avatarImageView) AvatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
@ -44,39 +43,41 @@ object AvatarRenderer {
private const val THUMBNAIL_SIZE = 250 private const val THUMBNAIL_SIZE = 250
@UiThread private val AVATAR_COLOR_LIST = listOf(
fun render(roomMember: RoomMember, imageView: ImageView) { R.color.avatar_color_1,
render(roomMember.avatarUrl, roomMember.displayName, imageView) R.color.avatar_color_2,
} R.color.avatar_color_3
)
@UiThread @UiThread
fun render(roomSummary: RoomSummary, imageView: ImageView) { fun render(roomSummary: RoomSummary, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.displayName, imageView) render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView)
} }
@UiThread @UiThread
fun render(avatarUrl: String?, name: String?, imageView: ImageView) { fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView)) render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))
} }
@UiThread @UiThread
fun render(context: Context, fun render(context: Context,
glideRequest: GlideRequests, glideRequest: GlideRequests,
avatarUrl: String?, avatarUrl: String?,
identifier: String,
name: String?, name: String?,
target: Target<Drawable>) { target: Target<Drawable>) {
if (name.isNullOrEmpty()) { if (name.isNullOrEmpty()) {
return return
} }
val placeholder = getPlaceholderDrawable(context, name) val placeholder = getPlaceholderDrawable(context, identifier, name)
buildGlideRequest(glideRequest, avatarUrl) buildGlideRequest(glideRequest, avatarUrl)
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
} }
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, text: String): Drawable { fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable {
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) val avatarColor = ContextCompat.getColor(context, getAvatarColor(identifier))
return if (text.isEmpty()) { return if (text.isEmpty()) {
TextDrawable.builder().buildRound("", avatarColor) TextDrawable.builder().buildRound("", avatarColor)
} else { } else {
@ -87,9 +88,21 @@ object AvatarRenderer {
} }
} }
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
private fun getAvatarColor(text: String? = null): Int {
var colorIndex: Long = 0
if (!text.isNullOrEmpty()) {
var sum: Long = 0
for (i in 0 until text.length) {
sum += text[i].toLong()
}
colorIndex = sum % AVATAR_COLOR_LIST.size
}
return AVATAR_COLOR_LIST[colorIndex.toInt()]
}
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver() val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)

View File

@ -35,6 +35,7 @@ class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
val isSelected = groupSummary.groupId == selected?.groupId val isSelected = groupSummary.groupId == selected?.groupId
groupSummaryItem { groupSummaryItem {
id(groupSummary.groupId) id(groupSummary.groupId)
groupId(groupSummary.groupId)
groupName(groupSummary.displayName) groupName(groupSummary.displayName)
selected(isSelected) selected(isSelected)
avatarUrl(groupSummary.avatarUrl) avatarUrl(groupSummary.avatarUrl)

View File

@ -29,6 +29,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() { abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
@EpoxyAttribute lateinit var groupName: CharSequence @EpoxyAttribute lateinit var groupName: CharSequence
@EpoxyAttribute lateinit var groupId: String
@EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
@ -37,7 +38,7 @@ abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
super.bind(holder) super.bind(holder)
holder.rootView.isSelected = selected holder.rootView.isSelected = selected
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
AvatarRenderer.render(avatarUrl, groupName.toString(), holder.avatarImageView) AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -37,6 +37,8 @@ import com.otaliastudios.autocomplete.Autocomplete
import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.AutocompleteCallback
import com.otaliastudios.autocomplete.CharPolicy import com.otaliastudios.autocomplete.CharPolicy
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R import im.vector.riotredesign.R
@ -65,7 +67,7 @@ import im.vector.riotredesign.features.home.room.detail.composer.TextComposerVie
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener import im.vector.riotredesign.features.home.room.detail.timeline.helper.EndlessRecyclerViewScrollListener
import im.vector.riotredesign.features.html.PillImageSpan import im.vector.riotredesign.features.html.PillImageSpan
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
import im.vector.riotredesign.features.media.MediaViewerActivity import im.vector.riotredesign.features.media.MediaViewerActivity
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
@ -379,11 +381,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
} }
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
val intent = MediaViewerActivity.newIntent(vectorBaseActivity, mediaData) val intent = MediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent) startActivity(intent)
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: ImageContentRenderer.Data, view: View) {
//TODO handle
}
// AutocompleteUserPresenter.Callback // AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {

View File

@ -25,15 +25,21 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.LoadingItemModel_
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.* import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineAsyncHelper
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent
import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
class TimelineEventController(private val dateFormatter: TimelineDateFormatter, class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
@ -44,7 +50,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
interface Callback { interface Callback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)
fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: ImageContentRenderer.Data, view: View)
} }
private val modelCache = arrayListOf<List<EpoxyModel<*>>>() private val modelCache = arrayListOf<List<EpoxyModel<*>>>()

View File

@ -28,7 +28,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageEmoteConte
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
@ -40,13 +40,13 @@ import im.vector.riotredesign.features.home.room.detail.timeline.helper.Timeline
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem
import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotredesign.features.html.EventHtmlRenderer import im.vector.riotredesign.features.html.EventHtmlRenderer
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
import me.gujun.android.span.span import me.gujun.android.span.span
class MessageItemFactory(private val colorProvider: ColorProvider, class MessageItemFactory(private val colorProvider: ColorProvider,
@ -79,15 +79,22 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
val avatarUrl = roomMember?.avatarUrl val avatarUrl = roomMember?.avatarUrl
val memberName = roomMember?.displayName ?: event.root.sender ?: "" val memberName = roomMember?.displayName ?: event.root.sender ?: ""
val formattedMemberName = span(memberName) { val formattedMemberName = span(memberName) {
textColor = colorProvider.getColor(colorIndexForSender(memberName)) textColor = colorProvider.getColor(getColorFor(event.root.sender ?: ""))
} }
val informationData = MessageInformationData(time, avatarUrl, formattedMemberName, showInformation) val informationData = MessageInformationData(eventId = eventId,
senderId = event.root.sender ?: "",
sendState = event.sendState,
time = time,
avatarUrl = avatarUrl,
memberName = formattedMemberName,
showInformation = showInformation)
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback)
is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback) is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback)
is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback)
else -> buildNotHandledMessageItem(messageContent) else -> buildNotHandledMessageItem(messageContent)
} }
} }
@ -97,31 +104,49 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return DefaultItem_().text(text) return DefaultItem_().text(text)
} }
private fun buildImageMessageItem(eventId: String, private fun buildImageMessageItem(messageContent: MessageImageContent,
messageContent: MessageImageContent,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageImageItem? { callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = MediaContentRenderer.Data( val data = ImageContentRenderer.Data(
messageContent.body, filename = messageContent.body,
url = messageContent.url, url = messageContent.url,
height = messageContent.info?.height, height = messageContent.info?.height,
maxHeight = maxHeight, maxHeight = maxHeight,
width = messageContent.info?.width, width = messageContent.info?.width,
maxWidth = maxWidth, maxWidth = maxWidth,
rotation = messageContent.info?.rotation, orientation = messageContent.info?.orientation,
orientation = messageContent.info?.orientation rotation = messageContent.info?.rotation
) )
return MessageImageItem_() return MessageImageVideoItem_()
.eventId(eventId) .playable(messageContent.info?.mimeType == "image/gif")
.informationData(informationData) .informationData(informationData)
.mediaData(data) .mediaData(data)
.clickListener { view -> callback?.onMediaClicked(data, view) } .clickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) }
} }
private fun buildTextMessageItem(sendState: SendState, private fun buildVideoMessageItem(messageContent: MessageVideoContent,
messageContent: MessageTextContent, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageImageVideoItem? {
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val data = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.info?.thumbnailUrl,
height = messageContent.info?.height,
maxHeight = maxHeight,
width = messageContent.info?.width,
maxWidth = maxWidth
)
return MessageImageVideoItem_()
.playable(true)
.informationData(informationData)
.mediaData(data)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, data, view) }
}
private fun buildTextMessageItem(messageContent: MessageTextContent,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
@ -129,15 +154,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
htmlRenderer.render(it) htmlRenderer.render(it)
} ?: messageContent.body } ?: messageContent.body
val textColor = if (sendState.isSent()) { val linkifiedBody = linkifyBody(bodyToUse, callback)
R.color.dark_grey
} else {
R.color.brown_grey
}
val formattedBody = span(bodyToUse) {
this.textColor = colorProvider.getColor(textColor)
}
val linkifiedBody = linkifyBody(formattedBody, callback)
return MessageTextItem_() return MessageTextItem_()
.message(linkifiedBody) .message(linkifiedBody)
.informationData(informationData) .informationData(informationData)
@ -184,8 +201,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
return spannable return spannable
} }
//Based on riot-web implementation
@ColorRes @ColorRes
private fun colorIndexForSender(sender: String): Int { private fun getColorFor(sender: String): Int {
var hash = 0 var hash = 0
var i = 0 var i = 0
var chr: Char var chr: Char
@ -210,6 +228,4 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
else -> R.color.username_8 else -> R.color.username_8
} }
} }
} }

View File

@ -25,7 +25,7 @@ import android.widget.TextView
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
import java.io.File import java.io.File
object ContentUploadStateTrackerBinder { object ContentUploadStateTrackerBinder {
@ -33,7 +33,7 @@ object ContentUploadStateTrackerBinder {
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>() private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
fun bind(eventId: String, fun bind(eventId: String,
mediaData: MediaContentRenderer.Data, mediaData: ImageContentRenderer.Data,
progressLayout: ViewGroup) { progressLayout: ViewGroup) {
Matrix.getInstance().currentSession?.also { session -> Matrix.getInstance().currentSession?.also { session ->
@ -56,7 +56,7 @@ object ContentUploadStateTrackerBinder {
} }
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
private val mediaData: MediaContentRenderer.Data) : ContentUploadStateTracker.UpdateListener { private val mediaData: ImageContentRenderer.Data) : ContentUploadStateTracker.UpdateListener {
override fun onUpdate(state: ContentUploadStateTracker.State) { override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) { when (state) {

View File

@ -35,7 +35,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
holder.timeView.visibility = View.VISIBLE holder.timeView.visibility = View.VISIBLE
holder.timeView.text = informationData.time holder.timeView.text = informationData.time
holder.memberNameView.text = informationData.memberName holder.memberNameView.text = informationData.memberName
AvatarRenderer.render(informationData.avatarUrl, informationData.memberName?.toString(), holder.avatarImageView) AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
} else { } else {
holder.avatarImageView.visibility = View.GONE holder.avatarImageView.visibility = View.GONE
holder.memberNameView.visibility = View.GONE holder.memberNameView.visibility = View.GONE
@ -43,6 +43,11 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
} }
} }
protected fun View.renderSendState() {
isClickable = informationData.sendState.isSent()
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
}
abstract class Holder : VectorEpoxyHolder() { abstract class Holder : VectorEpoxyHolder() {
abstract val avatarImageView: ImageView abstract val avatarImageView: ImageView
abstract val memberNameView: TextView abstract val memberNameView: TextView

View File

@ -24,27 +24,27 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.riotredesign.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_message) @EpoxyModelClass(layout = R.layout.item_timeline_event_image_video_message)
abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() { abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
@EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data @EpoxyAttribute lateinit var mediaData: ImageContentRenderer.Data
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute override lateinit var informationData: MessageInformationData @EpoxyAttribute override lateinit var informationData: MessageInformationData
@EpoxyAttribute var playable: Boolean = false
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) ImageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
ContentUploadStateTrackerBinder.bind(eventId, mediaData, holder.progressLayout) ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout)
holder.imageView.setOnClickListener(clickListener) holder.imageView.setOnClickListener(clickListener)
holder.imageView.isEnabled = !mediaData.isLocalFile() holder.imageView.renderSendState()
holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
ContentUploadStateTrackerBinder.unbind(eventId) ContentUploadStateTrackerBinder.unbind(informationData.eventId)
super.unbind(holder) super.unbind(holder)
} }
@ -52,8 +52,9 @@ abstract class MessageImageItem : AbsMessageItem<MessageImageItem.Holder>() {
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
override val memberNameView by bind<TextView>(R.id.messageMemberNameView) override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
override val timeView by bind<TextView>(R.id.messageTimeView) override val timeView by bind<TextView>(R.id.messageTimeView)
val progressLayout by bind<ViewGroup>(R.id.messageImageUploadProgressLayout) val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
val imageView by bind<ImageView>(R.id.messageImageView) val imageView by bind<ImageView>(R.id.messageThumbnailView)
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
} }
} }

View File

@ -16,7 +16,12 @@
package im.vector.riotredesign.features.home.room.detail.timeline.item package im.vector.riotredesign.features.home.room.detail.timeline.item
import im.vector.matrix.android.api.session.room.send.SendState
data class MessageInformationData( data class MessageInformationData(
val eventId: String,
val senderId: String,
val sendState: SendState,
val time: CharSequence? = null, val time: CharSequence? = null,
val avatarUrl: String?, val avatarUrl: String?,
val memberName: CharSequence? = null, val memberName: CharSequence? = null,

View File

@ -45,6 +45,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
TextViewCompat.getTextMetricsParams(holder.messageView), TextViewCompat.getTextMetricsParams(holder.messageView),
null) null)
holder.messageView.setTextFuture(textFuture) holder.messageView.setTextFuture(textFuture)
holder.messageView.renderSendState()
findPillsAndProcess { it.bind(holder.messageView) } findPillsAndProcess { it.bind(holder.messageView) }
} }

View File

@ -30,11 +30,12 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
@EpoxyAttribute var noticeText: CharSequence? = null @EpoxyAttribute var noticeText: CharSequence? = null
@EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var memberName: CharSequence? = null @EpoxyAttribute var memberName: CharSequence? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.noticeTextView.text = noticeText holder.noticeTextView.text = noticeText
AvatarRenderer.render(avatarUrl, memberName?.toString(), holder.avatarImageView) AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -79,6 +79,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
roomSummaryItem { roomSummaryItem {
id(roomSummary.roomId) id(roomSummary.roomId)
roomId(roomSummary.roomId)
roomName(roomSummary.displayName) roomName(roomSummary.displayName)
avatarUrl(roomSummary.avatarUrl) avatarUrl(roomSummary.avatarUrl)
selected(isSelected) selected(isSelected)

View File

@ -31,6 +31,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() { abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute lateinit var roomName: CharSequence @EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute lateinit var roomId: String
@EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var unreadCount: Int = 0 @EpoxyAttribute var unreadCount: Int = 0
@ -44,7 +45,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.rootView.isChecked = selected holder.rootView.isChecked = selected
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
holder.titleView.text = roomName holder.titleView.text = roomName
AvatarRenderer.render(avatarUrl, roomName.toString(), holder.avatarImageView) AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -52,7 +52,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
@UiThread @UiThread
fun bind(textView: TextView) { fun bind(textView: TextView) {
tv = WeakReference(textView) tv = WeakReference(textView)
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target) AvatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target)
} }
// ReplacementSpan ***************************************************************************** // ReplacementSpan *****************************************************************************
@ -105,7 +105,7 @@ class PillImageSpan(private val glideRequests: GlideRequests,
textStartPadding = textPadding textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height) setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size) setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName) chipIcon = AvatarRenderer.getPlaceholderDrawable(context, userId, displayName)
setBounds(0, 0, intrinsicWidth, intrinsicHeight) setBounds(0, 0, intrinsicWidth, intrinsicHeight)
} }
} }

View File

@ -27,7 +27,7 @@ import im.vector.riotredesign.core.glide.GlideApp
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import java.io.File import java.io.File
object MediaContentRenderer { object ImageContentRenderer {
@Parcelize @Parcelize
data class Data( data class Data(
@ -37,8 +37,8 @@ object MediaContentRenderer {
val maxHeight: Int, val maxHeight: Int,
val width: Int?, val width: Int?,
val maxWidth: Int, val maxWidth: Int,
val orientation: Int?, val orientation: Int? = null,
val rotation: Int? val rotation: Int? = null
) : Parcelable { ) : Parcelable {
fun isLocalFile(): Boolean { fun isLocalFile(): Boolean {
@ -66,6 +66,7 @@ object MediaContentRenderer {
GlideApp GlideApp
.with(imageView) .with(imageView)
.load(resolvedUrl) .load(resolvedUrl)
.dontAnimate()
.thumbnail(0.3f) .thumbnail(0.3f)
.into(imageView) .into(imageView)
} }
@ -73,9 +74,6 @@ object MediaContentRenderer {
fun render(data: Data, imageView: BigImageView) { fun render(data: Data, imageView: BigImageView) {
val (width, height) = processSize(data, Mode.THUMBNAIL) val (width, height) = processSize(data, Mode.THUMBNAIL)
val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver()
if (data.isLocalFile()) {
imageView.showImage(Uri.parse(data.url))
} else {
val fullSize = contentUrlResolver.resolveFullSize(data.url) val fullSize = contentUrlResolver.resolveFullSize(data.url)
val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE)
imageView.showImage( imageView.showImage(
@ -83,7 +81,6 @@ object MediaContentRenderer {
Uri.parse(fullSize) Uri.parse(fullSize)
) )
} }
}
private fun processSize(data: Data, mode: Mode): Pair<Int, Int> { private fun processSize(data: Data, mode: Mode): Pair<Int, Int> {
val maxImageWidth = data.maxWidth val maxImageWidth = data.maxWidth

View File

@ -33,18 +33,18 @@ class MediaViewerActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(im.vector.riotredesign.R.layout.activity_media_viewer) setContentView(im.vector.riotredesign.R.layout.activity_media_viewer)
val mediaData = intent.getParcelableExtra<MediaContentRenderer.Data>(EXTRA_MEDIA_DATA) val mediaData = intent.getParcelableExtra<ImageContentRenderer.Data>(EXTRA_MEDIA_DATA)
if (mediaData.url.isNullOrEmpty()) { if (mediaData.url.isNullOrEmpty()) {
finish() finish()
} else { } else {
configureToolbar(mediaViewerToolbar, mediaData) configureToolbar(mediaViewerToolbar, mediaData)
mediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) mediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
mediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) mediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
MediaContentRenderer.render(mediaData, mediaViewerImageView) ImageContentRenderer.render(mediaData, mediaViewerImageView)
} }
} }
private fun configureToolbar(toolbar: Toolbar, mediaData: MediaContentRenderer.Data) { private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.apply { supportActionBar?.apply {
title = mediaData.filename title = mediaData.filename
@ -57,7 +57,7 @@ class MediaViewerActivity : VectorBaseActivity() {
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
fun newIntent(context: Context, mediaData: MediaContentRenderer.Data): Intent { fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent {
return Intent(context, MediaViewerActivity::class.java).apply { return Intent(context, MediaViewerActivity::class.java).apply {
putExtra(EXTRA_MEDIA_DATA, mediaData) putExtra(EXTRA_MEDIA_DATA, mediaData)
} }

View File

@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2019 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
@ -43,6 +58,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:duplicateParentState="true"
android:textColor="@color/brown_grey" android:textColor="@color/brown_grey"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
@ -50,7 +66,7 @@
tools:text="@tools:sample/date/hhmm" /> tools:text="@tools:sample/date/hhmm" />
<ImageView <ImageView
android:id="@+id/messageImageView" android:id="@+id/messageThumbnailView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="64dp" android:layout_marginStart="64dp"
@ -59,13 +75,28 @@
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:layout_marginRight="32dp" android:layout_marginRight="32dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:duplicateParentState="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0" app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" /> app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
tools:layout_height="300dp" />
<ImageView
android:id="@+id/messageMediaPlayView"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_material_play_circle"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
app:layout_constraintStart_toStartOf="@id/messageThumbnailView"
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
tools:visibility="visible" />
<include <include
android:id="@+id/messageImageUploadProgressLayout" android:id="@+id/messageMediaUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout" layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="46dp" android:layout_height="46dp"
@ -78,7 +109,7 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageImageView" app:layout_constraintTop_toBottomOf="@+id/messageThumbnailView"
tools:visibility="visible" /> tools:visibility="visible" />

View File

@ -44,6 +44,7 @@
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:textColor="@color/brown_grey" android:textColor="@color/brown_grey"
android:duplicateParentState="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0" app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="@id/messageMemberNameView" app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
@ -56,6 +57,7 @@
android:layout_marginStart="64dp" android:layout_marginStart="64dp"
android:layout_marginLeft="64dp" android:layout_marginLeft="64dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:duplicateParentState="true"
android:textColor="@color/dark_grey" android:textColor="@color/dark_grey"
android:textSize="14sp" android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"