From c38a601bcc70b52f145bc2e7c971e0bd25a29242 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Apr 2019 15:40:07 +0200 Subject: [PATCH 1/8] Timeline : apply color for sender --- .../timeline/factory/MessageItemFactory.kt | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index f3596c4f..9df93a56 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory import android.text.Spannable import android.text.SpannableStringBuilder +import androidx.annotation.ColorRes import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.EventType @@ -76,14 +77,16 @@ class MessageItemFactory(private val colorProvider: ColorProvider, val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = roomMember?.avatarUrl - val memberName = roomMember?.displayName ?: event.root.sender - val informationData = MessageInformationData(time, avatarUrl, memberName, showInformation) + val memberName = roomMember?.displayName ?: event.root.sender ?: "" + val formattedMemberName = span(memberName) { + textColor = colorProvider.getColor(colorIndexForSender(memberName)) + } + val informationData = MessageInformationData(time, avatarUrl, formattedMemberName, showInformation) return when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback) is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback) - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) else -> buildNotHandledMessageItem(messageContent) } @@ -181,4 +184,32 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return spannable } + @ColorRes + private fun colorIndexForSender(sender: String): Int { + var hash = 0 + var i = 0 + var chr: Char + if (sender.isEmpty()) { + return R.color.username_1 + } + while (i < sender.length) { + chr = sender[i] + hash = (hash shl 5) - hash + chr.toInt() + hash = hash or 0 + i++ + } + val cI = Math.abs(hash) % 8 + 1 + return when (cI) { + 1 -> R.color.username_1 + 2 -> R.color.username_2 + 3 -> R.color.username_3 + 4 -> R.color.username_4 + 5 -> R.color.username_5 + 6 -> R.color.username_6 + 7 -> R.color.username_7 + else -> R.color.username_8 + } + } + + } \ No newline at end of file From 9c9c09db2b1747137e7a0ed8356d02e08cecc2fc Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 11 Apr 2019 19:19:52 +0200 Subject: [PATCH 2/8] Adjust colors for avatar and display names + start handling video in timeline --- .../user/AutocompleteUserController.kt | 1 + .../autocomplete/user/AutocompleteUserItem.kt | 13 ++- .../features/home/AvatarRenderer.kt | 37 ++++++--- .../home/group/GroupSummaryController.kt | 1 + .../features/home/group/GroupSummaryItem.kt | 3 +- .../home/room/detail/RoomDetailFragment.kt | 11 ++- .../timeline/TimelineEventController.kt | 13 ++- .../timeline/factory/MessageItemFactory.kt | 80 +++++++++++-------- .../helper/ContentUploadStateTrackerBinder.kt | 6 +- .../detail/timeline/item/AbsMessageItem.kt | 7 +- ...eImageItem.kt => MessageImageVideoItem.kt} | 25 +++--- .../timeline/item/MessageInformationData.kt | 5 ++ .../detail/timeline/item/MessageTextItem.kt | 1 + .../room/detail/timeline/item/NoticeItem.kt | 3 +- .../home/room/list/RoomSummaryController.kt | 1 + .../home/room/list/RoomSummaryItem.kt | 3 +- .../features/html/PillImageSpan.kt | 4 +- ...entRenderer.kt => ImageContentRenderer.kt} | 23 +++--- .../features/media/MediaViewerActivity.kt | 8 +- ...em_timeline_event_image_video_message.xml} | 41 ++++++++-- .../item_timeline_event_text_message.xml | 2 + 21 files changed, 188 insertions(+), 100 deletions(-) rename vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/{MessageImageItem.kt => MessageImageVideoItem.kt} (65%) rename vector/src/main/java/im/vector/riotredesign/features/media/{MediaContentRenderer.kt => ImageContentRenderer.kt} (88%) rename vector/src/main/res/layout/{item_timeline_event_image_message.xml => item_timeline_event_image_video_message.xml} (67%) diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt index bec9adb0..6f174e0d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserController.kt @@ -31,6 +31,7 @@ class AutocompleteUserController : TypedEpoxyController>() { data.forEach { user -> autocompleteUserItem { id(user.userId) + userId(user.userId) name(user.displayName) avatarUrl(user.avatarUrl) clickListener { _ -> diff --git a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt index 6678bff5..57694725 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/autocomplete/user/AutocompleteUserItem.kt @@ -29,18 +29,15 @@ import im.vector.riotredesign.features.home.AvatarRenderer @EpoxyModelClass(layout = R.layout.item_autocomplete_user) abstract class AutocompleteUserItem : VectorEpoxyModel() { - @EpoxyAttribute - var name: String? = null - @EpoxyAttribute - var avatarUrl: String? = null - @EpoxyAttribute - var clickListener: View.OnClickListener? = null + @EpoxyAttribute var name: String? = null + @EpoxyAttribute var userId: String = "" + @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { holder.view.setOnClickListener(clickListener) - holder.nameView.text = name - AvatarRenderer.render(avatarUrl, name, holder.avatarImageView) + AvatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index b64cadb1..70974710 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -29,7 +29,6 @@ import com.bumptech.glide.request.target.Target import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixPatterns 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.riotredesign.R import im.vector.riotredesign.core.glide.GlideApp @@ -44,39 +43,41 @@ object AvatarRenderer { private const val THUMBNAIL_SIZE = 250 - @UiThread - fun render(roomMember: RoomMember, imageView: ImageView) { - render(roomMember.avatarUrl, roomMember.displayName, imageView) - } + private val AVATAR_COLOR_LIST = listOf( + R.color.avatar_color_1, + R.color.avatar_color_2, + R.color.avatar_color_3 + ) @UiThread fun render(roomSummary: RoomSummary, imageView: ImageView) { - render(roomSummary.avatarUrl, roomSummary.displayName, imageView) + render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) } @UiThread - fun render(avatarUrl: String?, name: String?, imageView: ImageView) { - render(imageView.context, GlideApp.with(imageView), avatarUrl, name, DrawableImageViewTarget(imageView)) + fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) { + render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView)) } @UiThread fun render(context: Context, glideRequest: GlideRequests, avatarUrl: String?, + identifier: String, name: String?, target: Target) { if (name.isNullOrEmpty()) { return } - val placeholder = getPlaceholderDrawable(context, name) + val placeholder = getPlaceholderDrawable(context, identifier, name) buildGlideRequest(glideRequest, avatarUrl) .placeholder(placeholder) .into(target) } @AnyThread - fun getPlaceholderDrawable(context: Context, text: String): Drawable { - val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) + fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable { + val avatarColor = ContextCompat.getColor(context, getAvatarColor(identifier)) return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) } else { @@ -87,9 +88,21 @@ object AvatarRenderer { } } - // 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 { val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver() .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt index 0fc0c9d4..fe40b162 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt @@ -35,6 +35,7 @@ class GroupSummaryController : TypedEpoxyController() { val isSelected = groupSummary.groupId == selected?.groupId groupSummaryItem { id(groupSummary.groupId) + groupId(groupSummary.groupId) groupName(groupSummary.displayName) selected(isSelected) avatarUrl(groupSummary.avatarUrl) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt index 3acf7f0a..eef0e865 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryItem.kt @@ -29,6 +29,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer abstract class GroupSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var groupName: CharSequence + @EpoxyAttribute lateinit var groupId: String @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null @@ -37,7 +38,7 @@ abstract class GroupSummaryItem : VectorEpoxyModel() { super.bind(holder) holder.rootView.isSelected = selected holder.rootView.setOnClickListener { listener?.invoke() } - AvatarRenderer.render(avatarUrl, groupName.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 59cf99ab..d3319b74 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -37,6 +37,8 @@ import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy 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.user.model.User 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.helper.EndlessRecyclerViewScrollListener 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 kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* @@ -379,11 +381,16 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac 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) startActivity(intent) } + override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: ImageContentRenderer.Data, view: View) { + //TODO handle + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 618c15c2..b96ba1dd 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -25,15 +25,21 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel 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.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel 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.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.media.MediaContentRenderer +import im.vector.riotredesign.features.media.ImageContentRenderer class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, @@ -44,7 +50,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, interface Callback { fun onEventVisible(event: TimelineEvent) 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>>() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 9df93a56..884e8c34 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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.MessageNoticeContent 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.riotredesign.R 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.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.MessageImageItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem +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.MessageTextItem import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageTextItem_ 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 class MessageItemFactory(private val colorProvider: ColorProvider, @@ -79,15 +79,22 @@ class MessageItemFactory(private val colorProvider: ColorProvider, val avatarUrl = roomMember?.avatarUrl val memberName = roomMember?.displayName ?: event.root.sender ?: "" 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) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, callback) - is MessageTextContent -> buildTextMessageItem(event.sendState, messageContent, informationData, callback) - is MessageImageContent -> buildImageMessageItem(eventId, messageContent, informationData, callback) + is MessageTextContent -> buildTextMessageItem(messageContent, informationData, callback) + is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) + is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) else -> buildNotHandledMessageItem(messageContent) } } @@ -97,31 +104,49 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return DefaultItem_().text(text) } - private fun buildImageMessageItem(eventId: String, - messageContent: MessageImageContent, + private fun buildImageMessageItem(messageContent: MessageImageContent, informationData: MessageInformationData, - callback: TimelineEventController.Callback?): MessageImageItem? { + callback: TimelineEventController.Callback?): MessageImageVideoItem? { val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() - val data = MediaContentRenderer.Data( - messageContent.body, + val data = ImageContentRenderer.Data( + filename = messageContent.body, url = messageContent.url, height = messageContent.info?.height, maxHeight = maxHeight, width = messageContent.info?.width, maxWidth = maxWidth, - rotation = messageContent.info?.rotation, - orientation = messageContent.info?.orientation + orientation = messageContent.info?.orientation, + rotation = messageContent.info?.rotation ) - return MessageImageItem_() - .eventId(eventId) + return MessageImageVideoItem_() + .playable(messageContent.info?.mimeType == "image/gif") .informationData(informationData) .mediaData(data) - .clickListener { view -> callback?.onMediaClicked(data, view) } + .clickListener { view -> callback?.onImageMessageClicked(messageContent, data, view) } } - private fun buildTextMessageItem(sendState: SendState, - messageContent: MessageTextContent, + private fun buildVideoMessageItem(messageContent: MessageVideoContent, + 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, callback: TimelineEventController.Callback?): MessageTextItem? { @@ -129,15 +154,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider, htmlRenderer.render(it) } ?: messageContent.body - val textColor = if (sendState.isSent()) { - R.color.dark_grey - } else { - R.color.brown_grey - } - val formattedBody = span(bodyToUse) { - this.textColor = colorProvider.getColor(textColor) - } - val linkifiedBody = linkifyBody(formattedBody, callback) + val linkifiedBody = linkifyBody(bodyToUse, callback) return MessageTextItem_() .message(linkifiedBody) .informationData(informationData) @@ -184,8 +201,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider, return spannable } + //Based on riot-web implementation @ColorRes - private fun colorIndexForSender(sender: String): Int { + private fun getColorFor(sender: String): Int { var hash = 0 var i = 0 var chr: Char @@ -210,6 +228,4 @@ class MessageItemFactory(private val colorProvider: ColorProvider, else -> R.color.username_8 } } - - } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index d0d899ef..8d395668 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -25,7 +25,7 @@ import android.widget.TextView import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.riotredesign.R -import im.vector.riotredesign.features.media.MediaContentRenderer +import im.vector.riotredesign.features.media.ImageContentRenderer import java.io.File object ContentUploadStateTrackerBinder { @@ -33,7 +33,7 @@ object ContentUploadStateTrackerBinder { private val updateListeners = mutableMapOf() fun bind(eventId: String, - mediaData: MediaContentRenderer.Data, + mediaData: ImageContentRenderer.Data, progressLayout: ViewGroup) { Matrix.getInstance().currentSession?.also { session -> @@ -56,7 +56,7 @@ object ContentUploadStateTrackerBinder { } 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) { when (state) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index f4fe2e8d..17c2ea4a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -35,7 +35,7 @@ abstract class AbsMessageItem : VectorEpoxyModel() holder.timeView.visibility = View.VISIBLE holder.timeView.text = informationData.time 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 { holder.avatarImageView.visibility = View.GONE holder.memberNameView.visibility = View.GONE @@ -43,6 +43,11 @@ abstract class AbsMessageItem : VectorEpoxyModel() } } + protected fun View.renderSendState() { + isClickable = informationData.sendState.isSent() + alpha = if (informationData.sendState.isSent()) 1f else 0.5f + } + abstract class Holder : VectorEpoxyHolder() { abstract val avatarImageView: ImageView abstract val memberNameView: TextView diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt similarity index 65% rename from vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt rename to vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt index 15e3d5ff..27e464b1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageImageVideoItem.kt @@ -24,27 +24,27 @@ import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotredesign.R 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) -abstract class MessageImageItem : AbsMessageItem() { +@EpoxyModelClass(layout = R.layout.item_timeline_event_image_video_message) +abstract class MessageImageVideoItem : AbsMessageItem() { - @EpoxyAttribute lateinit var mediaData: MediaContentRenderer.Data - @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute lateinit var mediaData: ImageContentRenderer.Data @EpoxyAttribute override lateinit var informationData: MessageInformationData + @EpoxyAttribute var playable: Boolean = false @EpoxyAttribute var clickListener: View.OnClickListener? = null override fun bind(holder: Holder) { super.bind(holder) - MediaContentRenderer.render(mediaData, MediaContentRenderer.Mode.THUMBNAIL, holder.imageView) - ContentUploadStateTrackerBinder.bind(eventId, mediaData, holder.progressLayout) + ImageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView) + ContentUploadStateTrackerBinder.bind(informationData.eventId, mediaData, holder.progressLayout) holder.imageView.setOnClickListener(clickListener) - holder.imageView.isEnabled = !mediaData.isLocalFile() - holder.imageView.alpha = if (mediaData.isLocalFile()) 0.5f else 1f + holder.imageView.renderSendState() + holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE } override fun unbind(holder: Holder) { - ContentUploadStateTrackerBinder.unbind(eventId) + ContentUploadStateTrackerBinder.unbind(informationData.eventId) super.unbind(holder) } @@ -52,8 +52,9 @@ abstract class MessageImageItem : AbsMessageItem() { override val avatarImageView by bind(R.id.messageAvatarImageView) override val memberNameView by bind(R.id.messageMemberNameView) override val timeView by bind(R.id.messageTimeView) - val progressLayout by bind(R.id.messageImageUploadProgressLayout) - val imageView by bind(R.id.messageImageView) + val progressLayout by bind(R.id.messageMediaUploadProgressLayout) + val imageView by bind(R.id.messageThumbnailView) + val playContentView by bind(R.id.messageMediaPlayView) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt index f6ae60a9..286f11a5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -16,7 +16,12 @@ package im.vector.riotredesign.features.home.room.detail.timeline.item +import im.vector.matrix.android.api.session.room.send.SendState + data class MessageInformationData( + val eventId: String, + val senderId: String, + val sendState: SendState, val time: CharSequence? = null, val avatarUrl: String?, val memberName: CharSequence? = null, diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt index 5c5ebbf9..45ba0ec1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -45,6 +45,7 @@ abstract class MessageTextItem : AbsMessageItem() { TextViewCompat.getTextMetricsParams(holder.messageView), null) holder.messageView.setTextFuture(textFuture) + holder.messageView.renderSendState() findPillsAndProcess { it.bind(holder.messageView) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt index fe28892c..82156153 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/NoticeItem.kt @@ -30,11 +30,12 @@ abstract class NoticeItem : VectorEpoxyModel() { @EpoxyAttribute var noticeText: CharSequence? = null @EpoxyAttribute var avatarUrl: String? = null + @EpoxyAttribute var userId: String = "" @EpoxyAttribute var memberName: CharSequence? = null override fun bind(holder: Holder) { holder.noticeTextView.text = noticeText - AvatarRenderer.render(avatarUrl, memberName?.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, userId, memberName?.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 8c0b742d..24641da2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -79,6 +79,7 @@ class RoomSummaryController(private val stringProvider: StringProvider roomSummaryItem { id(roomSummary.roomId) + roomId(roomSummary.roomId) roomName(roomSummary.displayName) avatarUrl(roomSummary.avatarUrl) selected(isSelected) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt index 02a240d9..e6892610 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryItem.kt @@ -31,6 +31,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var roomName: CharSequence + @EpoxyAttribute lateinit var roomId: String @EpoxyAttribute var avatarUrl: String? = null @EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var unreadCount: Int = 0 @@ -44,7 +45,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.rootView.isChecked = selected holder.rootView.setOnClickListener { listener?.invoke() } holder.titleView.text = roomName - AvatarRenderer.render(avatarUrl, roomName.toString(), holder.avatarImageView) + AvatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) } class Holder : VectorEpoxyHolder() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index 5ab18531..e83e20e6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -52,7 +52,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, target) + AvatarRenderer.render(context, glideRequests, user?.avatarUrl, userId, displayName, target) } // ReplacementSpan ***************************************************************************** @@ -105,7 +105,7 @@ class PillImageSpan(private val glideRequests: GlideRequests, textStartPadding = textPadding setChipMinHeightResource(R.dimen.pill_min_height) setChipIconSizeResource(R.dimen.pill_avatar_size) - chipIcon = AvatarRenderer.getPlaceholderDrawable(context, displayName) + chipIcon = AvatarRenderer.getPlaceholderDrawable(context, userId, displayName) setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt similarity index 88% rename from vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt rename to vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt index fda67418..42c50e68 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaContentRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageContentRenderer.kt @@ -27,7 +27,7 @@ import im.vector.riotredesign.core.glide.GlideApp import kotlinx.android.parcel.Parcelize import java.io.File -object MediaContentRenderer { +object ImageContentRenderer { @Parcelize data class Data( @@ -37,8 +37,8 @@ object MediaContentRenderer { val maxHeight: Int, val width: Int?, val maxWidth: Int, - val orientation: Int?, - val rotation: Int? + val orientation: Int? = null, + val rotation: Int? = null ) : Parcelable { fun isLocalFile(): Boolean { @@ -66,6 +66,7 @@ object MediaContentRenderer { GlideApp .with(imageView) .load(resolvedUrl) + .dontAnimate() .thumbnail(0.3f) .into(imageView) } @@ -73,16 +74,12 @@ object MediaContentRenderer { fun render(data: Data, imageView: BigImageView) { val (width, height) = processSize(data, Mode.THUMBNAIL) val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() - if (data.isLocalFile()) { - imageView.showImage(Uri.parse(data.url)) - } else { - val fullSize = contentUrlResolver.resolveFullSize(data.url) - val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) - imageView.showImage( - Uri.parse(thumbnail), - Uri.parse(fullSize) - ) - } + val fullSize = contentUrlResolver.resolveFullSize(data.url) + val thumbnail = contentUrlResolver.resolveThumbnail(data.url, width, height, ContentUrlResolver.ThumbnailMethod.SCALE) + imageView.showImage( + Uri.parse(thumbnail), + Uri.parse(fullSize) + ) } private fun processSize(data: Data, mode: Mode): Pair { diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt index 1cbd16fe..9859b94b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt @@ -33,18 +33,18 @@ class MediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(im.vector.riotredesign.R.layout.activity_media_viewer) - val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) if (mediaData.url.isNullOrEmpty()) { finish() } else { configureToolbar(mediaViewerToolbar, mediaData) mediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) 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) supportActionBar?.apply { title = mediaData.filename @@ -57,7 +57,7 @@ class MediaViewerActivity : VectorBaseActivity() { 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 { putExtra(EXTRA_MEDIA_DATA, mediaData) } diff --git a/vector/src/main/res/layout/item_timeline_event_image_message.xml b/vector/src/main/res/layout/item_timeline_event_image_video_message.xml similarity index 67% rename from vector/src/main/res/layout/item_timeline_event_image_message.xml rename to vector/src/main/res/layout/item_timeline_event_image_video_message.xml index 21c7b12f..b1da5882 100644 --- a/vector/src/main/res/layout/item_timeline_event_image_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_image_video_message.xml @@ -1,4 +1,19 @@ - + + + app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" + tools:layout_height="300dp" /> + + + diff --git a/vector/src/main/res/layout/item_timeline_event_text_message.xml b/vector/src/main/res/layout/item_timeline_event_text_message.xml index 5d300442..bd6f9f89 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message.xml @@ -44,6 +44,7 @@ android:layout_marginStart="8dp" android:layout_marginLeft="8dp" android:textColor="@color/brown_grey" + android:duplicateParentState="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintTop_toTopOf="@id/messageMemberNameView" @@ -56,6 +57,7 @@ android:layout_marginStart="64dp" android:layout_marginLeft="64dp" android:layout_marginBottom="8dp" + android:duplicateParentState="true" android:textColor="@color/dark_grey" android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" From 657f4d3e9c297a7192d2964e3f9ba663cd593f67 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Apr 2019 12:38:02 +0200 Subject: [PATCH 3/8] Timeline : handle file/audio message --- .../core/platform/VectorBaseActivity.kt | 9 +- .../home/room/detail/RoomDetailFragment.kt | 15 +- .../timeline/TimelineEventController.kt | 4 + .../timeline/factory/MessageItemFactory.kt | 26 ++++ .../detail/timeline/item/MessageFileItem.kt | 57 ++++++++ .../item_timeline_event_file_message.xml | 135 ++++++++++++++++++ 6 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_file_message.xml diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt index 388d3425..20a2c091 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt @@ -32,6 +32,7 @@ import com.bumptech.glide.util.Util import com.google.android.material.snackbar.Snackbar import im.vector.riotredesign.BuildConfig import im.vector.riotredesign.R +import im.vector.riotredesign.core.utils.toast import im.vector.riotredesign.features.rageshake.BugReportActivity import im.vector.riotredesign.features.rageshake.BugReporter import im.vector.riotredesign.features.rageshake.RageShake @@ -284,9 +285,9 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { * PUBLIC METHODS * ========================================================================================== */ - protected fun showSnackbar(message: String) { + fun showSnackbar(message: String) { coordinatorLayout?.let { - Snackbar.make(it, message, Snackbar.LENGTH_SHORT) + Snackbar.make(it, message, Snackbar.LENGTH_SHORT).show() } } @@ -294,8 +295,8 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { * Temporary method * ========================================================================================== */ - protected fun notImplemented() { - showSnackbar(getString(R.string.not_implemented)) + fun notImplemented() { + toast(getString(R.string.not_implemented)) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index d3319b74..8a8ef50e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -37,6 +37,8 @@ import com.otaliastudios.autocomplete.Autocomplete import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent 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 @@ -371,7 +373,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac .show() } -// TimelineEventController.Callback ************************************************************ + // TimelineEventController.Callback ************************************************************ override fun onUrlClicked(url: String) { homePermalinkHandler.launch(url) @@ -387,11 +389,18 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac } override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: ImageContentRenderer.Data, view: View) { - //TODO handle + vectorBaseActivity.notImplemented() } + override fun onFileMessageClicked(messageFileContent: MessageFileContent) { + vectorBaseActivity.notImplemented() + } -// AutocompleteUserPresenter.Callback + override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) { + vectorBaseActivity.notImplemented() + } + + // AutocompleteUserPresenter.Callback override fun onQueryUsers(query: CharSequence?) { textComposerViewModel.process(TextComposerActions.QueryUsers(query)) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index b96ba1dd..67654861 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -25,6 +25,8 @@ import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent 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 @@ -52,6 +54,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: ImageContentRenderer.Data, view: View) + fun onFileMessageClicked(messageFileContent: MessageFileContent) + fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) } private val modelCache = arrayListOf>>() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 884e8c34..28bf49da 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -23,8 +23,10 @@ import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent 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.MessageTextContent @@ -40,6 +42,8 @@ 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.item.DefaultItem import im.vector.riotredesign.features.home.room.detail.timeline.item.DefaultItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.riotredesign.features.home.room.detail.timeline.item.MessageInformationData @@ -95,10 +99,32 @@ class MessageItemFactory(private val colorProvider: ColorProvider, is MessageImageContent -> buildImageMessageItem(messageContent, informationData, callback) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, callback) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, callback) + is MessageFileContent -> buildFileMessageItem(messageContent, informationData, callback) + is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, callback) else -> buildNotHandledMessageItem(messageContent) } } + private fun buildAudioMessageItem(messageContent: MessageAudioContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): MessageFileItem? { + return MessageFileItem_() + .informationData(informationData) + .filename(messageContent.body) + .iconRes(R.drawable.filetype_audio) + .clickListener { _ -> callback?.onAudioMessageClicked(messageContent) } + } + + private fun buildFileMessageItem(messageContent: MessageFileContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?): MessageFileItem? { + return MessageFileItem_() + .informationData(informationData) + .filename(messageContent.body) + .iconRes(R.drawable.filetype_attachment) + .clickListener { _ -> callback?.onFileMessageClicked(messageContent) } + } + private fun buildNotHandledMessageItem(messageContent: MessageContent): DefaultItem? { val text = "${messageContent.type} message events are not yet handled" return DefaultItem_().text(text) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt new file mode 100644 index 00000000..edd3c779 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -0,0 +1,57 @@ +/* + * 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. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.item + +import android.graphics.Paint +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotredesign.R + +@EpoxyModelClass(layout = R.layout.item_timeline_event_file_message) +abstract class MessageFileItem : AbsMessageItem() { + + @EpoxyAttribute var filename: CharSequence = "" + @EpoxyAttribute @DrawableRes var iconRes: Int = 0 + @EpoxyAttribute override lateinit var informationData: MessageInformationData + @EpoxyAttribute var clickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.fileLayout.renderSendState() + holder.filenameView.text = filename + holder.fileImageView.setImageResource(iconRes) + holder.filenameView.setOnClickListener(clickListener) + holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG) + } + + + class Holder : AbsMessageItem.Holder() { + override val avatarImageView by bind(R.id.messageAvatarImageView) + override val memberNameView by bind(R.id.messageMemberNameView) + override val timeView by bind(R.id.messageTimeView) + val fileLayout by bind(R.id.messageFileLayout) + val fileImageView by bind(R.id.messageFileImageView) + val filenameView by bind(R.id.messageFilenameView) + } + + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_file_message.xml b/vector/src/main/res/layout/item_timeline_event_file_message.xml new file mode 100644 index 00000000..83bc4815 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_file_message.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 2c83ba0824f102ddf01d8a76e771740507730c3a Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 12 Apr 2019 13:46:59 +0200 Subject: [PATCH 4/8] Timeline : start to handle video media. Probably to amend --- .../session/content/UploadContentWorker.kt | 18 ++++++ vector/build.gradle | 1 + vector/src/main/AndroidManifest.xml | 3 +- .../home/room/detail/RoomDetailFragment.kt | 11 ++-- .../timeline/TimelineEventController.kt | 3 +- .../timeline/factory/MessageItemFactory.kt | 14 ++++- ...ctivity.kt => ImageMediaViewerActivity.kt} | 16 ++--- .../features/media/VideoContentRenderer.kt | 41 ++++++++++++ .../media/VideoMediaViewerActivity.kt | 62 +++++++++++++++++++ ...er.xml => activity_image_media_viewer.xml} | 4 +- .../layout/activity_video_media_viewer.xml | 48 ++++++++++++++ 11 files changed, 202 insertions(+), 19 deletions(-) rename vector/src/main/java/im/vector/riotredesign/features/media/{MediaViewerActivity.kt => ImageMediaViewerActivity.kt} (78%) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt rename vector/src/main/res/layout/{activity_media_viewer.xml => activity_image_media_viewer.xml} (89%) create mode 100644 vector/src/main/res/layout/activity_video_media_viewer.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index f864e725..84adddd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -24,8 +24,11 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessageFileContent 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.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.util.WorkerParamsFactory @@ -69,6 +72,9 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) val messageContent: MessageContent = event.content.toModel() ?: return event val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url) + is MessageVideoContent -> messageContent.update(url) + is MessageFileContent -> messageContent.update(url) + is MessageAudioContent -> messageContent.update(url) else -> messageContent } return event.copy(content = updatedContent.toContent()) @@ -78,6 +84,18 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) return copy(url = url) } + private fun MessageVideoContent.update(url: String): MessageVideoContent { + return copy(url = url) + } + + private fun MessageFileContent.update(url: String): MessageFileContent { + return copy(url = url) + } + + private fun MessageAudioContent.update(url: String): MessageAudioContent { + return copy(url = url) + } + } diff --git a/vector/build.gradle b/vector/build.gradle index 691090f0..bdf95803 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -187,6 +187,7 @@ dependencies { implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version" implementation "com.github.bumptech.glide:glide:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version" + implementation 'com.danikula:videocache:2.7.1' // Badge for compatibility implementation 'me.leolin:ShortcutBadger:1.1.2@aar' diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index bea4280e..7aac94fa 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ - + @@ -37,6 +37,7 @@ android:name=".features.settings.VectorSettingsActivity" android:label="@string/title_activity_settings" android:windowSoftInputMode="adjustResize" /> + callback?.onVideoMessageClicked(messageContent, data, view) } + .mediaData(thumbnailData) + .clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) } } private fun buildTextMessageItem(messageContent: MessageTextContent, diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt similarity index 78% rename from vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt rename to vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt index 9859b94b..902c2f9b 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/media/MediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/media/ImageMediaViewerActivity.kt @@ -25,22 +25,22 @@ import androidx.appcompat.widget.Toolbar import com.github.piasy.biv.indicator.progresspie.ProgressPieIndicator import com.github.piasy.biv.view.GlideImageViewFactory import im.vector.riotredesign.core.platform.VectorBaseActivity -import kotlinx.android.synthetic.main.activity_media_viewer.* +import kotlinx.android.synthetic.main.activity_image_media_viewer.* -class MediaViewerActivity : VectorBaseActivity() { +class ImageMediaViewerActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(im.vector.riotredesign.R.layout.activity_media_viewer) + setContentView(im.vector.riotredesign.R.layout.activity_image_media_viewer) val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) if (mediaData.url.isNullOrEmpty()) { finish() } else { - configureToolbar(mediaViewerToolbar, mediaData) - mediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) - mediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) - ImageContentRenderer.render(mediaData, mediaViewerImageView) + configureToolbar(imageMediaViewerToolbar, mediaData) + imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory()) + imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator()) + ImageContentRenderer.render(mediaData, imageMediaViewerImageView) } } @@ -58,7 +58,7 @@ class MediaViewerActivity : VectorBaseActivity() { private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent { - return Intent(context, MediaViewerActivity::class.java).apply { + return Intent(context, ImageMediaViewerActivity::class.java).apply { putExtra(EXTRA_MEDIA_DATA, mediaData) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt new file mode 100644 index 00000000..577b6570 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoContentRenderer.kt @@ -0,0 +1,41 @@ +/* + * 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. + */ + +package im.vector.riotredesign.features.media + +import android.os.Parcelable +import android.widget.ImageView +import android.widget.VideoView +import im.vector.matrix.android.api.Matrix +import kotlinx.android.parcel.Parcelize + +object VideoContentRenderer { + + @Parcelize + data class Data( + val filename: String, + val videoUrl: String?, + val thumbnailMediaData: ImageContentRenderer.Data + ) : Parcelable + + fun render(data: Data, thumbnailView: ImageView, videoView: VideoView) { + val contentUrlResolver = Matrix.getInstance().currentSession!!.contentUrlResolver() + val resolvedUrl = contentUrlResolver.resolveFullSize(data.videoUrl) + videoView.setVideoPath(resolvedUrl) + videoView.start() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt new file mode 100644 index 00000000..0630a2cb --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/media/VideoMediaViewerActivity.kt @@ -0,0 +1,62 @@ +/* + * 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. + */ + +package im.vector.riotredesign.features.media + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.widget.Toolbar +import im.vector.riotredesign.core.platform.VectorBaseActivity +import kotlinx.android.synthetic.main.activity_video_media_viewer.* + + +class VideoMediaViewerActivity : VectorBaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(im.vector.riotredesign.R.layout.activity_video_media_viewer) + val mediaData = intent.getParcelableExtra(EXTRA_MEDIA_DATA) + if (mediaData.videoUrl.isNullOrEmpty()) { + finish() + } else { + configureToolbar(videoMediaViewerToolbar, mediaData) + VideoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerVideoView) + } + } + + private fun configureToolbar(toolbar: Toolbar, mediaData: VideoContentRenderer.Data) { + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = mediaData.filename + setHomeButtonEnabled(true) + setDisplayHomeAsUpEnabled(true) + } + } + + companion object { + + private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA" + + fun newIntent(context: Context, mediaData: VideoContentRenderer.Data): Intent { + return Intent(context, VideoMediaViewerActivity::class.java).apply { + putExtra(EXTRA_MEDIA_DATA, mediaData) + } + } + } + + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/activity_media_viewer.xml b/vector/src/main/res/layout/activity_image_media_viewer.xml similarity index 89% rename from vector/src/main/res/layout/activity_media_viewer.xml rename to vector/src/main/res/layout/activity_image_media_viewer.xml index 9927331e..3644714b 100644 --- a/vector/src/main/res/layout/activity_media_viewer.xml +++ b/vector/src/main/res/layout/activity_image_media_viewer.xml @@ -6,7 +6,7 @@ android:orientation="vertical"> + + + + + + + + + + + + + + \ No newline at end of file From dab80466c5503e93b0e0d3ab6fdba7ce11b82178 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 16 Apr 2019 17:24:28 +0200 Subject: [PATCH 5/8] Timeline : extract video thumbnail and upload it --- .../session/content/ContentAttachmentData.kt | 4 +- .../content/ContentUploadStateTracker.kt | 10 ++- .../internal/session/content/ContentModule.kt | 2 +- .../DefaultContentUploadStateTracker.kt | 34 +++++----- .../{ContentUploader.kt => FileUploader.kt} | 66 +++++++++--------- .../session/content/ThumbnailExtractor.kt | 68 +++++++++++++++++++ .../session/content/UploadContentWorker.kt | 68 +++++++++++++++---- .../room/send/LocalEchoEventFactory.kt | 33 +++++++-- 8 files changed, 212 insertions(+), 73 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/{ContentUploader.kt => FileUploader.kt} (51%) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt index 82a4a762..c8dca869 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentAttachmentData.kt @@ -27,8 +27,8 @@ data class ContentAttachmentData( val height: Long? = 0, val width: Long? = 0, val name: String? = null, - val path: String? = null, - val mimeType: String? = null, + val path: String, + val mimeType: String, val type: Type ) : Parcelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt index 0d88e5fa..a08060e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/content/ContentUploadStateTracker.kt @@ -18,9 +18,15 @@ package im.vector.matrix.android.api.session.content interface ContentUploadStateTracker { - fun track(eventId: String, updateListener: UpdateListener) + fun track(key: String, updateListener: UpdateListener) - fun untrack(eventId: String, updateListener: UpdateListener) + fun untrack(key: String, updateListener: UpdateListener) + + fun setFailure(key: String) + + fun setSuccess(key: String) + + fun setProgress(key: String, current: Long, total: Long) interface UpdateListener { fun onUpdate(state: State) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt index b149cd38..67ff2d7b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentModule.kt @@ -31,7 +31,7 @@ internal class ContentModule { } scope(DefaultSession.SCOPE) { - ContentUploader(get(), get(), get() as DefaultContentUploadStateTracker) + FileUploader(get(), get()) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt index 1e6ca3c1..66bd5a82 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/DefaultContentUploadStateTracker.kt @@ -23,42 +23,42 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker internal class DefaultContentUploadStateTracker : ContentUploadStateTracker { private val mainHandler = Handler(Looper.getMainLooper()) - private val progressByEvent = mutableMapOf() - private val listenersByEvent = mutableMapOf>() + private val states = mutableMapOf() + private val listeners = mutableMapOf>() - override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { - val listeners = listenersByEvent[eventId] ?: ArrayList() + override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + val listeners = listeners[key] ?: ArrayList() listeners.add(updateListener) - listenersByEvent[eventId] = listeners - val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle + this.listeners[key] = listeners + val currentState = states[key] ?: ContentUploadStateTracker.State.Idle mainHandler.post { updateListener.onUpdate(currentState) } } - override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { - listenersByEvent[eventId]?.apply { + override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { + listeners[key]?.apply { remove(updateListener) } } - internal fun setFailure(eventId: String) { + override fun setFailure(key: String) { val failure = ContentUploadStateTracker.State.Failure - updateState(eventId, failure) + updateState(key, failure) } - internal fun setSuccess(eventId: String) { + override fun setSuccess(key: String) { val success = ContentUploadStateTracker.State.Success - updateState(eventId, success) + updateState(key, success) } - internal fun setProgress(eventId: String, current: Long, total: Long) { + override fun setProgress(key: String, current: Long, total: Long) { val progressData = ContentUploadStateTracker.State.ProgressData(current, total) - updateState(eventId, progressData) + updateState(key, progressData) } - private fun updateState(eventId: String, state: ContentUploadStateTracker.State) { - progressByEvent[eventId] = state + private fun updateState(key: String, state: ContentUploadStateTracker.State) { + states[key] = state mainHandler.post { - listenersByEvent[eventId]?.also { listeners -> + listeners[key]?.also { listeners -> listeners.forEach { it.onUpdate(state) } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt similarity index 51% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt index 42dff4f0..1ec18127 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ContentUploader.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/FileUploader.kt @@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.content import arrow.core.Try import arrow.core.Try.Companion.raise import im.vector.matrix.android.api.auth.data.SessionParams -import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.network.ProgressRequestBody import okhttp3.HttpUrl @@ -31,44 +30,51 @@ import java.io.File import java.io.IOException -internal class ContentUploader(private val okHttpClient: OkHttpClient, - private val sessionParams: SessionParams, - private val contentUploadProgressTracker: DefaultContentUploadStateTracker) { +internal class FileUploader(private val okHttpClient: OkHttpClient, + private val sessionParams: SessionParams) { + + private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" private val moshi = MoshiProvider.providesMoshi() private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) - fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try { - if (attachment.path == null || attachment.mimeType == null) { - return raise(RuntimeException()) - } - val file = File(attachment.path) - val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload" - val urlBuilder = HttpUrl.parse(urlString)?.newBuilder() - ?: return raise(RuntimeException()) + fun uploadFile(file: File, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): Try { + + val uploadBody = RequestBody.create(MediaType.parse(mimeType), file) + return upload(uploadBody, filename, progressListener) + + } + + fun uploadByteArray(byteArray: ByteArray, + filename: String?, + mimeType: String, + progressListener: ProgressRequestBody.Listener? = null): Try { + + val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray) + return upload(uploadBody, filename, progressListener) + + } + + + private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try { + val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException()) val httpUrl = urlBuilder - .addQueryParameter( - "filename", attachment.name - ).build() + .addQueryParameter("filename", filename) + .build() - val requestBody = RequestBody.create( - MediaType.parse(attachment.mimeType), - file - ) - val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - contentUploadProgressTracker.setProgress(eventId, current, total) - } - }) + val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody val request = Request.Builder() .url(httpUrl) - .post(progressRequestBody) + .post(requestBody) .build() - val result = Try { + return Try { okHttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { throw IOException() @@ -80,11 +86,7 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient, } } } - if (result.isFailure()) { - contentUploadProgressTracker.setFailure(eventId) - } else { - contentUploadProgressTracker.setSuccess(eventId) - } - return result + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt new file mode 100644 index 00000000..8fc6f5f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/ThumbnailExtractor.kt @@ -0,0 +1,68 @@ +/* + * 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. + */ + +package im.vector.matrix.android.internal.session.content + +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import android.provider.MediaStore +import im.vector.matrix.android.api.session.content.ContentAttachmentData +import java.io.ByteArrayOutputStream +import java.io.File + +internal object ThumbnailExtractor { + + class ThumbnailData( + val width: Int, + val height: Int, + val size: Long, + val bytes: ByteArray, + val mimeType: String + ) + + fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? { + val file = File(attachment.path) + if (!file.exists() || !file.isFile) { + return null + } + return if (attachment.type == ContentAttachmentData.Type.VIDEO) { + extractVideoThumbnail(attachment) + } else { + null + } + } + + private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? { + val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND) + val outputStream = ByteArrayOutputStream() + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + val thumbnailWidth = thumbnail.width + val thumbnailHeight = thumbnail.height + val thumbnailSize = outputStream.size() + val thumbnailData = ThumbnailData( + width = thumbnailWidth, + height = thumbnailHeight, + size = thumbnailSize.toLong(), + bytes = outputStream.toByteArray(), + mimeType = "image/jpeg" + ) + thumbnail.recycle() + outputStream.reset() + return thumbnailData + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 84adddd5..4eeb124d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -21,6 +21,7 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel @@ -30,14 +31,19 @@ import im.vector.matrix.android.api.session.room.model.message.MessageFileConten 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.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject +import timber.log.Timber +import java.io.File + internal class UploadContentWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params), MatrixKoinComponent { - private val mediaUploader by inject() + private val fileUploader by inject() + private val contentUploadProgressTracker by inject() @JsonClass(generateAdapter = true) internal data class Params( @@ -50,29 +56,63 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() - if (params.event.eventId == null) { - return Result.failure() + val eventId = params.event.eventId ?: return Result.failure() + val attachment = params.attachment + + val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment) + val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure() + var uploadedThumbnailUrl: String? = null + + if (thumbnailData != null) { + fileUploader + .uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType) + .fold( + { Timber.e(it) }, + { uploadedThumbnailUrl = it.contentUri } + ) } - return mediaUploader - .uploadFile(params.event.eventId, params.attachment) - .fold({ handleFailure() }, { handleSuccess(params, it) }) + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + contentUploadProgressTracker.setProgress(eventId, current, total) + } + } + return fileUploader + .uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) + .fold( + { handleFailure(params) }, + { handleSuccess(params, it.contentUri, uploadedThumbnailUrl) } + ) } - private fun handleFailure(): Result { - return Result.retry() + private fun createAttachmentFile(attachment: ContentAttachmentData): File? { + return try { + File(attachment.path) + } catch (e: Exception) { + Timber.e(e) + null + } } - private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result { - val event = updateEvent(params.event, contentUploadResponse.contentUri) + private fun handleFailure(params: Params): Result { + contentUploadProgressTracker.setFailure(params.event.eventId!!) + return Result.failure() + } + + private fun handleSuccess(params: Params, + attachmentUrl: String, + thumbnailUrl: String?): Result { + contentUploadProgressTracker.setFailure(params.event.eventId!!) + val event = updateEvent(params.event, attachmentUrl, thumbnailUrl) val sendParams = SendEventWorker.Params(params.roomId, event) return Result.success(WorkerParamsFactory.toData(sendParams)) } - private fun updateEvent(event: Event, url: String): Event { + private fun updateEvent(event: Event, url: String, thumbnailUrl: String? = null): Event { val messageContent: MessageContent = event.content.toModel() ?: return event val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url) - is MessageVideoContent -> messageContent.update(url) + is MessageVideoContent -> messageContent.update(url, thumbnailUrl) is MessageFileContent -> messageContent.update(url) is MessageAudioContent -> messageContent.update(url) else -> messageContent @@ -84,8 +124,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) return copy(url = url) } - private fun MessageVideoContent.update(url: String): MessageVideoContent { - return copy(url = url) + private fun MessageVideoContent.update(url: String, thumbnailUrl: String?): MessageVideoContent { + return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl)) } private fun MessageFileContent.update(url: String): MessageFileContent { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index 56051503..778a49ec 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.send +import android.media.MediaMetadataRetriever import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event @@ -30,7 +31,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageConte import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo +import im.vector.matrix.android.internal.session.content.ThumbnailExtractor internal class LocalEchoEventFactory(private val credentials: Credentials) { @@ -53,7 +56,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { type = MessageType.MSGTYPE_IMAGE, body = attachment.name ?: "image", info = ImageInfo( - mimeType = attachment.mimeType ?: "image/png", + mimeType = attachment.mimeType, width = attachment.width?.toInt() ?: 0, height = attachment.height?.toInt() ?: 0, size = attachment.size.toInt() @@ -64,15 +67,35 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) { } private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { + val mediaDataRetriever = MediaMetadataRetriever() + mediaDataRetriever.setDataSource(attachment.path) + + // Use frame to calculate height and width as we are sure to get the right ones + val firstFrame = mediaDataRetriever.frameAtTime + val height = firstFrame.height + val width = firstFrame.width + mediaDataRetriever.release() + + val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let { + ThumbnailInfo( + width = it.width, + height = it.height, + size = it.size, + mimeType = it.mimeType + ) + } val content = MessageVideoContent( type = MessageType.MSGTYPE_VIDEO, body = attachment.name ?: "video", info = VideoInfo( - mimeType = attachment.mimeType ?: "video/mpeg", - width = attachment.width?.toInt() ?: 0, - height = attachment.height?.toInt() ?: 0, + mimeType = attachment.mimeType, + width = width, + height = height, size = attachment.size, - duration = attachment.duration?.toInt() ?: 0 + duration = attachment.duration?.toInt() ?: 0, + // Glide will be able to use the local path and extract a thumbnail. + thumbnailUrl = attachment.path, + thumbnailInfo = thumbnailInfo ), url = attachment.path ) From b3e2eca43d8923eba8de3b1ab1c60e7b3cf81337 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 17 Apr 2019 22:37:25 +0200 Subject: [PATCH 6/8] Timeline : start to handle merging room member events. Need to get better perf. --- .../home/room/detail/RoomDetailActions.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 11 +-- .../home/room/detail/RoomDetailViewModel.kt | 18 ++-- .../timeline/TimelineEventController.kt | 94 +++++++++++------- .../timeline/factory/RoomMemberItemFactory.kt | 21 ++-- .../timeline/factory/TimelineItemFactory.kt | 61 ++++++++---- .../helper/TimelineDisplayableEvents.kt | 24 ++++- ...lineEventVisibilityStateChangedListener.kt | 36 +++++++ .../timeline/item/RoomMemberMergedItem.kt | 99 +++++++++++++++++++ ...item_timeline_event_room_member_merged.xml | 73 ++++++++++++++ .../vector_message_merge_avatar_list.xml | 57 +++++++++++ 11 files changed, 413 insertions(+), 83 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_room_member_merged.xml create mode 100644 vector/src/main/res/layout/vector_message_merge_avatar_list.xml diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 38c436b5..3b595077 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -25,7 +25,7 @@ sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() object IsDisplayed : RoomDetailActions() - data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() + data class EventsDisplayed(val events: List) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 66babeac..f2922d43 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -50,12 +50,7 @@ import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.VectorBaseFragment -import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA -import im.vector.riotredesign.core.utils.checkPermissions -import im.vector.riotredesign.core.utils.openCamera +import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter @@ -381,8 +376,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac homePermalinkHandler.launch(url) } - override fun onEventVisible(event: TimelineEvent) { - roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) + override fun onEventsVisible(events: List) { + roomDetailViewModel.process(RoomDetailActions.EventsDisplayed(events)) } override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index d2662d79..da2754cf 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -44,7 +44,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val displayedEventsObservable = BehaviorRelay.create() private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { @@ -69,11 +69,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.EventsDisplayed -> handleEventDisplayed(action) + is RoomDetailActions.LoadMore -> handleLoadMore(action) } } @@ -196,7 +196,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.sendMedias(attachments) } - private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { + private fun handleEventDisplayed(action: RoomDetailActions.EventsDisplayed) { displayedEventsObservable.accept(action) } @@ -215,8 +215,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.maxBy { it.event.displayIndex } - mostRecentEvent?.event?.root?.eventId?.let { eventId -> + val mostRecentEvent = actions.map { it.events }.flatten().maxBy { it.displayIndex } + mostRecentEvent?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 2d48766b..010ab48a 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -24,7 +24,6 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.VisibilityState import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent @@ -32,15 +31,18 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVideoConte import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ -import im.vector.riotredesign.core.epoxy.VectorEpoxyModel 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.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.TimelineEventVisibilityStateChangedListener import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent +import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextSameTypeEvents import im.vector.riotredesign.features.home.room.detail.timeline.item.DaySeparatorItem_ +import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer @@ -51,7 +53,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback { - fun onEventVisible(event: TimelineEvent) + fun onEventsVisible(events: List) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) @@ -59,7 +61,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) } - private val modelCache = arrayListOf>>() + + private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -72,7 +75,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, override fun onChanged(position: Int, count: Int, payload: Any?) { assertUpdateCallbacksAllowed() (position until (position + count)).forEach { - modelCache[it] = emptyList() + modelCache[it] = null } requestModelBuild() } @@ -88,11 +91,18 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized override fun onInserted(position: Int, count: Int) { assertUpdateCallbacksAllowed() - if (modelCache.isNotEmpty() && position == modelCache.size) { - modelCache[position - 1] = emptyList() + // When adding backwards we need to clear some events + if (position == modelCache.size) { + val previousCachedModel = modelCache.getOrNull(position - 1) + if (previousCachedModel != null) { + val numberOfMergedEvents = previousCachedModel.numberOfMergedEvents + for (i in 0..numberOfMergedEvents) { + modelCache[position - 1 - i] = null + } + } } (0 until count).forEach { - modelCache.add(position, emptyList()) + modelCache.add(position, null) } requestModelBuild() } @@ -161,53 +171,63 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized private fun getModels(): List> { (0 until modelCache.size).forEach { position -> - if (modelCache[position].isEmpty()) { - modelCache[position] = buildItemModels(position, currentSnapshot) + if (modelCache[position] == null) { + buildAndCacheItemsAt(position) } } - return modelCache.flatten() + return modelCache + .map { listOf(it?.eventModel, it?.formattedDayModel) } + .flatten() + .filterNotNull() } - private fun buildItemModels(currentPosition: Int, items: List): List> { - val epoxyModels = ArrayList>() + private fun buildAndCacheItemsAt(position: Int) { + val buildItemModelsResult = buildItemModels(position, currentSnapshot) + modelCache[position] = buildItemModelsResult + val prevResult = modelCache.getOrNull(position + 1) + if (prevResult != null && prevResult.eventModel is RoomMemberMergedItem && buildItemModelsResult.eventModel is RoomMemberMergedItem) { + buildItemModelsResult.eventModel.isCollapsed = prevResult.eventModel.isCollapsed + } + for (skipItemPosition in 0 until buildItemModelsResult.numberOfMergedEvents) { + val dumbModelsResult = CacheItemData(numberOfMergedEvents = buildItemModelsResult.numberOfMergedEvents) + modelCache[position + 1 + skipItemPosition] = dumbModelsResult + } + } + + private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] - val nextEvent = items.nextDisplayableEvent(currentPosition) + val mergeableEvents = if (event.canBeMerged()) items.nextSameTypeEvents(currentPosition, minSize = 2) else emptyList() + val mergedEvents = listOf(event) + mergeableEvents + val nextDisplayableEvent = items.nextDisplayableEvent(currentPosition + mergeableEvents.size) val date = event.root.localDateTime() - val nextDate = nextEvent?.root?.localDateTime() + val nextDate = nextDisplayableEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() + val visibilityStateChangedListener = TimelineEventVisibilityStateChangedListener(callback, mergedEvents) + val epoxyModelId = mergedEvents.joinToString(separator = "_") { it.localId } - timelineItemFactory.create(event, nextEvent, callback).also { - it.id(event.localId) - it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) - epoxyModels.add(it) + val eventModel = timelineItemFactory.create(event, mergeableEvents, nextDisplayableEvent, callback, visibilityStateChangedListener).also { + it.id(epoxyModelId) } - if (addDaySeparator) { + val daySeparatorItem = if (addDaySeparator) { val formattedDay = dateFormatter.formatMessageDay(date) - val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) - epoxyModels.add(daySeparatorItem) + DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) + } else { + null } - return epoxyModels + return CacheItemData(eventModel, daySeparatorItem, mergeableEvents.size) } private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { - val shouldAdd = timeline?.let { - it.hasMoreToLoad(direction) - } ?: false + val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false addIf(shouldAdd, this@TimelineEventController) } } -private class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val event: TimelineEvent) - : VectorEpoxyModel.OnVisibilityStateChangedListener { +private data class CacheItemData( + val eventModel: EpoxyModel<*>? = null, + val formattedDayModel: EpoxyModel<*>? = null, + val numberOfMergedEvents: Int = 0 +) - override fun onVisibilityStateChanged(visibilityState: Int) { - if (visibilityState == VisibilityState.VISIBLE) { - callback?.onEventVisible(event) - } - } - - -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt index 46c036ee..5d2efcba 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt @@ -34,6 +34,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { val roomMember = event.roomMember ?: return null val noticeText = buildRoomMemberNotice(event) ?: return null return NoticeItem_() + .userId(event.root.sender ?: "") .noticeText(noticeText) .avatarUrl(roomMember.avatarUrl) .memberName(roomMember.displayName) @@ -57,9 +58,9 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { val displayNameText = when { prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName) - eventContent?.displayName.isNullOrEmpty() -> + eventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName) - else -> + else -> stringProvider.getString(R.string.notice_display_name_changed_from, event.root.sender, prevEventContent?.displayName, eventContent?.displayName) } @@ -86,20 +87,20 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { // TODO get userId val selfUserId: String = "" when { - eventContent.thirdPartyInvite != null -> + eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.root.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) - event.root.stateKey.isNullOrEmpty() -> + event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName) - else -> + else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName) } } - Membership.JOIN == eventContent?.membership -> + Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName) - Membership.LEAVE == eventContent?.membership -> + Membership.LEAVE == eventContent?.membership -> // 2 cases here: this member may have left voluntarily or they may have been "left" by someone else ie. kicked return if (TextUtils.equals(event.root.sender, event.root.stateKey)) { if (prevEventContent?.membership == Membership.INVITE) { @@ -116,11 +117,11 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { } else { null } - Membership.BAN == eventContent?.membership -> + Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName) - Membership.KNOCK == eventContent?.membership -> + Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName) - else -> null + else -> null } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 234490e4..f980cb60 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -16,11 +16,14 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory +import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.EmptyItem_ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val roomNameItemFactory: RoomNameItemFactory, @@ -31,33 +34,57 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val defaultItemFactory: DefaultItemFactory) { fun create(event: TimelineEvent, + mergeableEvents: List, nextEvent: TimelineEvent?, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { + callback: TimelineEventController.Callback?, + visibilityStateChangedListener: TimelineEventVisibilityStateChangedListener): EpoxyModelWithHolder<*> { val computedModel = try { - when (event.root.type) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) - EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) - EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) - EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) - EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) + if (mergeableEvents.isNotEmpty()) { + createMergedEvent(event, mergeableEvents, visibilityStateChangedListener) + } else { + when (event.root.type) { + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) + EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) + EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) + EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) + EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> callItemFactory.create(event) + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER -> callItemFactory.create(event) - EventType.ENCRYPTED, - EventType.ENCRYPTION, - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) + EventType.ENCRYPTED, + EventType.ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) - else -> null + else -> null + } } } catch (e: Exception) { defaultItemFactory.create(event, e) } - return computedModel ?: EmptyItem_() + return (computedModel ?: EmptyItem_()).apply { + if (this is VectorEpoxyModel) { + this.setOnVisibilityStateChanged(visibilityStateChangedListener) + } + } + } + + private fun createMergedEvent(event: TimelineEvent, + mergeableEvents: List, + visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener): RoomMemberMergedItem { + + val events = listOf(event) + mergeableEvents + // We are reversing it as it does add items on a LinearLayout + val roomMemberItems = events.reversed().mapNotNull { + roomMemberItemFactory.create(it)?.apply { + id(it.localId) + } + } + return RoomMemberMergedItem(events, roomMemberItems, visibilityStateChangedListener) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 565f4254..deb4ad44 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -48,8 +48,30 @@ fun List.filterDisplayableEvents(): List { } } +fun TimelineEvent.canBeMerged(): Boolean { + return root.type == EventType.STATE_ROOM_MEMBER +} + +fun List.nextSameTypeEvents(index: Int, minSize: Int): List { + if (index >= size - 1) { + return emptyList() + } + val timelineEvent = this[index] + val nextSubList = subList(index + 1, size) + val indexOfFirstDifferentEventType = nextSubList.indexOfFirst { it.root.type != timelineEvent.root.type } + val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) { + nextSubList + } else { + nextSubList.subList(0, indexOfFirstDifferentEventType) + } + if (sameTypeEvents.size < minSize) { + return emptyList() + } + return sameTypeEvents +} + fun List.nextDisplayableEvent(index: Int): TimelineEvent? { - return if (index == size - 1) { + return if (index >= size - 1) { null } else { subList(index + 1, this.size).firstOrNull { it.isDisplayable() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt new file mode 100644 index 00000000..0b329ec8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -0,0 +1,36 @@ +/* + * + * * 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. + * + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import com.airbnb.epoxy.VisibilityState +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController + +class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, + private val events: List) + : VectorEpoxyModel.OnVisibilityStateChangedListener { + + override fun onVisibilityStateChanged(visibilityState: Int) { + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onEventsVisible(events) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt new file mode 100644 index 00000000..11f21745 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt @@ -0,0 +1,99 @@ +/* + * + * * 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. + * + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.children +import com.airbnb.epoxy.EpoxyModelGroup +import com.airbnb.epoxy.ModelGroupHolder +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.R +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.AvatarRenderer + +class RoomMemberMergedItem(val events: List, + private val roomMemberItems: List, + private val visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener +) : EpoxyModelGroup(R.layout.item_timeline_event_room_member_merged, roomMemberItems) { + + private val distinctRoomMemberItems = roomMemberItems.distinctBy { it.userId } + var isCollapsed = true + set(value) { + field = value + updateModelVisibility() + } + + init { + updateModelVisibility() + } + + override fun onVisibilityStateChanged(visibilityState: Int, view: ModelGroupHolder) { + super.onVisibilityStateChanged(visibilityState, view) + visibilityStateChangedListener.onVisibilityStateChanged(visibilityState) + } + + override fun bind(holder: ModelGroupHolder) { + super.bind(holder) + val expandView = holder.rootView.findViewById(R.id.itemMergedExpandTextView) + val summaryView = holder.rootView.findViewById(R.id.itemMergedSummaryTextView) + val separatorView = holder.rootView.findViewById(R.id.itemMergedSeparatorView) + val avatarListView = holder.rootView.findViewById(R.id.itemMergedAvatarListView) + if (isCollapsed) { + val summary = holder.rootView.resources.getQuantityString(R.plurals.membership_changes, roomMemberItems.size, roomMemberItems.size) + summaryView.text = summary + summaryView.visibility = View.VISIBLE + avatarListView.visibility = View.VISIBLE + avatarListView.children.forEachIndexed { index, view -> + val roomMemberItem = distinctRoomMemberItems.getOrNull(index) + if (roomMemberItem != null && view is ImageView) { + view.visibility = View.VISIBLE + AvatarRenderer.render(roomMemberItem.avatarUrl, roomMemberItem.userId, roomMemberItem.memberName?.toString(), view) + } else { + view.visibility = View.GONE + } + } + separatorView.visibility = View.GONE + expandView.setText(R.string.merged_events_expand) + } else { + avatarListView.visibility = View.INVISIBLE + summaryView.visibility = View.GONE + separatorView.visibility = View.VISIBLE + expandView.setText(R.string.merged_events_collapse) + } + expandView.setOnClickListener { _ -> + isCollapsed = !isCollapsed + updateModelVisibility() + bind(holder) + } + } + + private fun updateModelVisibility() { + roomMemberItems.forEach { + if (isCollapsed) { + it.hide() + } else { + it.show() + } + } + } + +} \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml b/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml new file mode 100644 index 00000000..2d33bcac --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/vector_message_merge_avatar_list.xml b/vector/src/main/res/layout/vector_message_merge_avatar_list.xml new file mode 100644 index 00000000..d12d8266 --- /dev/null +++ b/vector/src/main/res/layout/vector_message_merge_avatar_list.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file From 287feace1286f01499d95b872bbd6f5cf09335ce Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 18 Apr 2019 19:09:15 +0200 Subject: [PATCH 7/8] Timeline : merged events are now handled directly within the recyclerview and do not need a LinearLayout. --- .../home/room/detail/RoomDetailActions.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 4 +- .../home/room/detail/RoomDetailViewModel.kt | 18 +-- .../timeline/TimelineEventController.kt | 127 +++++++++++------- .../timeline/factory/TimelineItemFactory.kt | 61 +++------ .../helper/TimelineDisplayableEvents.kt | 25 +++- ...lineEventVisibilityStateChangedListener.kt | 4 +- .../detail/timeline/item/MergedHeaderItem.kt | 94 +++++++++++++ .../timeline/item/RoomMemberMergedItem.kt | 99 -------------- ... => item_timeline_event_merged_header.xml} | 26 ++-- 10 files changed, 232 insertions(+), 228 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt delete mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt rename vector/src/main/res/layout/{item_timeline_event_room_member_merged.xml => item_timeline_event_merged_header.xml} (76%) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 3b595077..38c436b5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -25,7 +25,7 @@ sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() data class SendMedia(val mediaFiles: List) : RoomDetailActions() object IsDisplayed : RoomDetailActions() - data class EventsDisplayed(val events: List) : RoomDetailActions() + data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index f2922d43..6af6c184 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -376,8 +376,8 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac homePermalinkHandler.launch(url) } - override fun onEventsVisible(events: List) { - roomDetailViewModel.process(RoomDetailActions.EventsDisplayed(events)) + override fun onEventVisible(event: TimelineEvent) { + roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event)) } override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index da2754cf..d2662d79 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -44,7 +44,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val room = session.getRoom(initialState.roomId)!! private val roomId = initialState.roomId private val eventId = initialState.eventId - private val displayedEventsObservable = BehaviorRelay.create() + private val displayedEventsObservable = BehaviorRelay.create() private val timeline = room.createTimeline(eventId, TimelineDisplayableEvents.DISPLAYABLE_TYPES) companion object : MvRxViewModelFactory { @@ -69,11 +69,11 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() - is RoomDetailActions.SendMedia -> handleSendMedia(action) - is RoomDetailActions.EventsDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) + is RoomDetailActions.LoadMore -> handleLoadMore(action) } } @@ -196,7 +196,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.sendMedias(attachments) } - private fun handleEventDisplayed(action: RoomDetailActions.EventsDisplayed) { + private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) { displayedEventsObservable.accept(action) } @@ -215,8 +215,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, .buffer(1, TimeUnit.SECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> - val mostRecentEvent = actions.map { it.events }.flatten().maxBy { it.displayIndex } - mostRecentEvent?.root?.eventId?.let { eventId -> + val mostRecentEvent = actions.maxBy { it.event.displayIndex } + mostRecentEvent?.event?.root?.eventId?.let { eventId -> room.setReadReceipt(eventId, callback = object : MatrixCallback {}) } }) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 010ab48a..39240dce 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -33,18 +33,13 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ 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.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.TimelineEventVisibilityStateChangedListener -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider -import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged -import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent -import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextSameTypeEvents +import im.vector.riotredesign.features.home.room.detail.timeline.helper.* +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.home.room.detail.timeline.item.RoomMemberMergedItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.MergedHeaderItem import im.vector.riotredesign.features.media.ImageContentRenderer import im.vector.riotredesign.features.media.VideoContentRenderer +import org.threeten.bp.LocalDateTime class TimelineEventController(private val dateFormatter: TimelineDateFormatter, private val timelineItemFactory: TimelineItemFactory, @@ -53,7 +48,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener { interface Callback { - fun onEventsVisible(events: List) + fun onEventVisible(event: TimelineEvent) fun onUrlClicked(url: String) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) @@ -61,8 +56,10 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) } - + private val collapsedEventIds = linkedSetOf() + private val mergeItemCollapseStates = HashMap() private val modelCache = arrayListOf() + private var currentSnapshot: List = emptyList() private var inSubmitList: Boolean = false private var timeline: Timeline? = null @@ -91,16 +88,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized override fun onInserted(position: Int, count: Int) { assertUpdateCallbacksAllowed() - // When adding backwards we need to clear some events - if (position == modelCache.size) { - val previousCachedModel = modelCache.getOrNull(position - 1) - if (previousCachedModel != null) { - val numberOfMergedEvents = previousCachedModel.numberOfMergedEvents - for (i in 0..numberOfMergedEvents) { - modelCache[position - 1 - i] = null - } - } - } (0 until count).forEach { modelCache.add(position, null) } @@ -138,7 +125,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, .id("forward_loading_item") .addWhen(Timeline.Direction.FORWARDS) - val timelineModels = getModels() add(timelineModels) @@ -171,51 +157,90 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, @Synchronized private fun getModels(): List> { (0 until modelCache.size).forEach { position -> - if (modelCache[position] == null) { - buildAndCacheItemsAt(position) + // Should be build if not cached or if cached but contains mergedHeader or formattedDay + // We then are sure we always have items up to date. + if (modelCache[position] == null + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { + modelCache[position] = buildItemModels(position, currentSnapshot) } } return modelCache - .map { listOf(it?.eventModel, it?.formattedDayModel) } + .map { + val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) { + null + } else { + it.eventModel + } + listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) + } .flatten() .filterNotNull() } - private fun buildAndCacheItemsAt(position: Int) { - val buildItemModelsResult = buildItemModels(position, currentSnapshot) - modelCache[position] = buildItemModelsResult - val prevResult = modelCache.getOrNull(position + 1) - if (prevResult != null && prevResult.eventModel is RoomMemberMergedItem && buildItemModelsResult.eventModel is RoomMemberMergedItem) { - buildItemModelsResult.eventModel.isCollapsed = prevResult.eventModel.isCollapsed - } - for (skipItemPosition in 0 until buildItemModelsResult.numberOfMergedEvents) { - val dumbModelsResult = CacheItemData(numberOfMergedEvents = buildItemModelsResult.numberOfMergedEvents) - modelCache[position + 1 + skipItemPosition] = dumbModelsResult - } - } private fun buildItemModels(currentPosition: Int, items: List): CacheItemData { val event = items[currentPosition] - val mergeableEvents = if (event.canBeMerged()) items.nextSameTypeEvents(currentPosition, minSize = 2) else emptyList() - val mergedEvents = listOf(event) + mergeableEvents - val nextDisplayableEvent = items.nextDisplayableEvent(currentPosition + mergeableEvents.size) - + val nextEvent = items.nextDisplayableEvent(currentPosition) val date = event.root.localDateTime() - val nextDate = nextDisplayableEvent?.root?.localDateTime() + val nextDate = nextEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val visibilityStateChangedListener = TimelineEventVisibilityStateChangedListener(callback, mergedEvents) - val epoxyModelId = mergedEvents.joinToString(separator = "_") { it.localId } - val eventModel = timelineItemFactory.create(event, mergeableEvents, nextDisplayableEvent, callback, visibilityStateChangedListener).also { - it.id(epoxyModelId) + val eventModel = timelineItemFactory.create(event, nextEvent, callback).also { + it.id(event.localId) + it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) } - val daySeparatorItem = if (addDaySeparator) { + val mergedHeaderModel = buildMergedHeaderItem(event, nextEvent, items, addDaySeparator, currentPosition) + val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) + + return CacheItemData(event.localId, eventModel, mergedHeaderModel, daySeparatorItem) + } + + private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { + return if (addDaySeparator) { val formattedDay = dateFormatter.formatMessageDay(date) DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) } else { null } - return CacheItemData(eventModel, daySeparatorItem, mergeableEvents.size) + } + + private fun buildMergedHeaderItem(event: TimelineEvent, + nextEvent: TimelineEvent?, + items: List, + addDaySeparator: Boolean, + currentPosition: Int): MergedHeaderItem? { + return if (!event.canBeMerged() || (nextEvent?.root?.type == event.root.type && !addDaySeparator)) { + null + } else { + val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) + if (prevSameTypeEvents.isEmpty()) { + null + } else { + val mergedEvents = (listOf(event) + prevSameTypeEvents) + val mergedData = mergedEvents.map { + val roomMember = event.roomMember + MergedHeaderItem.Data( + userId = event.root.sender ?: "", + avatarUrl = roomMember?.avatarUrl, + memberName = roomMember?.displayName ?: "", + eventId = it.localId + ) + } + val mergedEventIds = mergedEvents.map { it.localId } + val mergeId = mergedEventIds.joinToString(separator = "_") { it } + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { true } + if (isCollapsed) { + collapsedEventIds.addAll(mergedEventIds) + } else { + collapsedEventIds.removeAll(mergedEventIds) + } + MergedHeaderItem(isCollapsed, mergeId, mergedData) { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + } + } + } } private fun LoadingItemModel_.addWhen(direction: Timeline.Direction) { @@ -226,8 +251,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, } private data class CacheItemData( + val localId: String, val eventModel: EpoxyModel<*>? = null, - val formattedDayModel: EpoxyModel<*>? = null, - val numberOfMergedEvents: Int = 0 + val mergedHeaderModel: MergedHeaderItem? = null, + val formattedDayModel: DaySeparatorItem? = null ) - diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index f980cb60..d5354434 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -16,14 +16,11 @@ package im.vector.riotredesign.features.home.room.detail.timeline.factory -import com.airbnb.epoxy.EpoxyModelWithHolder import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.EmptyItem_ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener -import im.vector.riotredesign.features.home.room.detail.timeline.item.RoomMemberMergedItem class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val roomNameItemFactory: RoomNameItemFactory, @@ -34,57 +31,33 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val defaultItemFactory: DefaultItemFactory) { fun create(event: TimelineEvent, - mergeableEvents: List, nextEvent: TimelineEvent?, - callback: TimelineEventController.Callback?, - visibilityStateChangedListener: TimelineEventVisibilityStateChangedListener): EpoxyModelWithHolder<*> { + callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { val computedModel = try { - if (mergeableEvents.isNotEmpty()) { - createMergedEvent(event, mergeableEvents, visibilityStateChangedListener) - } else { - when (event.root.type) { - EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) - EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) - EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) - EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) - EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) + when (event.root.type) { + EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) + EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) + EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) + EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) + EventType.STATE_HISTORY_VISIBILITY -> roomHistoryVisibilityItemFactory.create(event) - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_ANSWER -> callItemFactory.create(event) + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_ANSWER -> callItemFactory.create(event) - EventType.ENCRYPTED, - EventType.ENCRYPTION, - EventType.STATE_ROOM_THIRD_PARTY_INVITE, - EventType.STICKER, - EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) + EventType.ENCRYPTED, + EventType.ENCRYPTION, + EventType.STATE_ROOM_THIRD_PARTY_INVITE, + EventType.STICKER, + EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) - else -> null - } + else -> null } } catch (e: Exception) { defaultItemFactory.create(event, e) } - return (computedModel ?: EmptyItem_()).apply { - if (this is VectorEpoxyModel) { - this.setOnVisibilityStateChanged(visibilityStateChangedListener) - } - } - } - - private fun createMergedEvent(event: TimelineEvent, - mergeableEvents: List, - visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener): RoomMemberMergedItem { - - val events = listOf(event) + mergeableEvents - // We are reversing it as it does add items on a LinearLayout - val roomMemberItems = events.reversed().mapNotNull { - roomMemberItemFactory.create(it)?.apply { - id(it.localId) - } - } - return RoomMemberMergedItem(events, roomMemberItems, visibilityStateChangedListener) + return (computedModel ?: EmptyItem_()) } } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index deb4ad44..53d63f58 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail.timeline.helper import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.riotredesign.core.extensions.localDateTime object TimelineDisplayableEvents { @@ -58,11 +59,21 @@ fun List.nextSameTypeEvents(index: Int, minSize: Int): List.nextSameTypeEvents(index: Int, minSize: Int): List.prevSameTypeEvents(index: Int, minSize: Int): List { + val prevSub = subList(0, index + 1) + return prevSub + .reversed() + .nextSameTypeEvents(0, minSize) + .reversed() +} + fun List.nextDisplayableEvent(index: Int): TimelineEvent? { return if (index >= size - 1) { null diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt index 0b329ec8..f103c18d 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt @@ -24,12 +24,12 @@ import im.vector.riotredesign.core.epoxy.VectorEpoxyModel import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController class TimelineEventVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?, - private val events: List) + private val event: TimelineEvent) : VectorEpoxyModel.OnVisibilityStateChangedListener { override fun onVisibilityStateChanged(visibilityState: Int) { if (visibilityState == VisibilityState.VISIBLE) { - callback?.onEventsVisible(events) + callback?.onEventVisible(event) } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt new file mode 100644 index 00000000..d5e05c11 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -0,0 +1,94 @@ +/* + * + * * 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. + * + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.children +import im.vector.riotredesign.R +import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.features.home.AvatarRenderer + +data class MergedHeaderItem(private val isCollapsed: Boolean, + private val mergeId: String, + private val mergeData: List, + private val onCollapsedStateChanged: (Boolean) -> Unit +) : VectorEpoxyModel() { + + private val distinctMergeData = mergeData.distinctBy { it.userId } + + init { + id(mergeId) + } + + override fun getDefaultLayout(): Int { + return R.layout.item_timeline_event_merged_header + } + + override fun createNewHolder(): Holder { + return Holder() + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.expandView.setOnClickListener { + onCollapsedStateChanged(!isCollapsed) + } + if (isCollapsed) { + val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, mergeData.size, mergeData.size) + holder.summaryView.text = summary + holder.summaryView.visibility = View.VISIBLE + holder.avatarListView.visibility = View.VISIBLE + holder.avatarListView.children.forEachIndexed { index, view -> + val data = distinctMergeData.getOrNull(index) + if (data != null && view is ImageView) { + view.visibility = View.VISIBLE + AvatarRenderer.render(data.avatarUrl, data.userId, data.memberName, view) + } else { + view.visibility = View.GONE + } + } + holder.separatorView.visibility = View.GONE + holder.expandView.setText(R.string.merged_events_expand) + } else { + holder.avatarListView.visibility = View.INVISIBLE + holder.summaryView.visibility = View.GONE + holder.separatorView.visibility = View.VISIBLE + holder.expandView.setText(R.string.merged_events_collapse) + } + } + + data class Data( + val eventId: String, + val userId: String, + val memberName: String, + val avatarUrl: String? + ) + + class Holder : VectorEpoxyHolder() { + val expandView by bind(R.id.itemMergedExpandTextView) + val summaryView by bind(R.id.itemMergedSummaryTextView) + val separatorView by bind(R.id.itemMergedSeparatorView) + val avatarListView by bind(R.id.itemMergedAvatarListView) + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt deleted file mode 100644 index 11f21745..00000000 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RoomMemberMergedItem.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * - * * 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. - * - */ - -package im.vector.riotredesign.features.home.room.detail.timeline.item - -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.view.children -import com.airbnb.epoxy.EpoxyModelGroup -import com.airbnb.epoxy.ModelGroupHolder -import im.vector.matrix.android.api.session.room.timeline.TimelineEvent -import im.vector.riotredesign.R -import im.vector.riotredesign.core.epoxy.VectorEpoxyModel -import im.vector.riotredesign.features.home.AvatarRenderer - -class RoomMemberMergedItem(val events: List, - private val roomMemberItems: List, - private val visibilityStateChangedListener: VectorEpoxyModel.OnVisibilityStateChangedListener -) : EpoxyModelGroup(R.layout.item_timeline_event_room_member_merged, roomMemberItems) { - - private val distinctRoomMemberItems = roomMemberItems.distinctBy { it.userId } - var isCollapsed = true - set(value) { - field = value - updateModelVisibility() - } - - init { - updateModelVisibility() - } - - override fun onVisibilityStateChanged(visibilityState: Int, view: ModelGroupHolder) { - super.onVisibilityStateChanged(visibilityState, view) - visibilityStateChangedListener.onVisibilityStateChanged(visibilityState) - } - - override fun bind(holder: ModelGroupHolder) { - super.bind(holder) - val expandView = holder.rootView.findViewById(R.id.itemMergedExpandTextView) - val summaryView = holder.rootView.findViewById(R.id.itemMergedSummaryTextView) - val separatorView = holder.rootView.findViewById(R.id.itemMergedSeparatorView) - val avatarListView = holder.rootView.findViewById(R.id.itemMergedAvatarListView) - if (isCollapsed) { - val summary = holder.rootView.resources.getQuantityString(R.plurals.membership_changes, roomMemberItems.size, roomMemberItems.size) - summaryView.text = summary - summaryView.visibility = View.VISIBLE - avatarListView.visibility = View.VISIBLE - avatarListView.children.forEachIndexed { index, view -> - val roomMemberItem = distinctRoomMemberItems.getOrNull(index) - if (roomMemberItem != null && view is ImageView) { - view.visibility = View.VISIBLE - AvatarRenderer.render(roomMemberItem.avatarUrl, roomMemberItem.userId, roomMemberItem.memberName?.toString(), view) - } else { - view.visibility = View.GONE - } - } - separatorView.visibility = View.GONE - expandView.setText(R.string.merged_events_expand) - } else { - avatarListView.visibility = View.INVISIBLE - summaryView.visibility = View.GONE - separatorView.visibility = View.VISIBLE - expandView.setText(R.string.merged_events_collapse) - } - expandView.setOnClickListener { _ -> - isCollapsed = !isCollapsed - updateModelVisibility() - bind(holder) - } - } - - private fun updateModelVisibility() { - roomMemberItems.forEach { - if (isCollapsed) { - it.hide() - } else { - it.show() - } - } - } - -} \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml b/vector/src/main/res/layout/item_timeline_event_merged_header.xml similarity index 76% rename from vector/src/main/res/layout/item_timeline_event_room_member_merged.xml rename to vector/src/main/res/layout/item_timeline_event_merged_header.xml index 2d33bcac..862a15db 100644 --- a/vector/src/main/res/layout/item_timeline_event_room_member_merged.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_header.xml @@ -3,6 +3,8 @@ @@ -11,8 +13,8 @@ layout="@layout/vector_message_merge_avatar_list" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="80dp" - android:layout_marginLeft="80dp" + android:layout_marginStart="64dp" + android:layout_marginLeft="64dp" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" android:layout_marginRight="16dp" @@ -25,18 +27,17 @@ android:id="@+id/itemMergedExpandTextView" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" + android:paddingRight="8dp" android:layout_marginTop="2dp" - android:layout_marginEnd="24dp" - android:layout_marginRight="24dp" - android:layout_marginBottom="8dp" + android:paddingLeft="8dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" android:text="@string/merged_events_expand" android:textColor="?attr/colorAccent" android:textSize="14sp" android:textStyle="italic" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/itemMergedAvatarListView" - android:layout_marginLeft="8dp" /> + app:layout_constraintTop_toTopOf="parent" /> - - \ No newline at end of file From 694df9d8455217373d5630d48d46b27270f920e9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 30 Apr 2019 19:55:55 +0200 Subject: [PATCH 8/8] Timeline : fix some timeline rendering issues (senderName, merge item, left event). Still need to work on it. --- .../session/room/timeline/TimelineTest.kt | 4 +- .../session/room/timeline/TimelineEvent.kt | 3 +- .../database/helper/ChunkEntityHelper.kt | 20 ++++----- .../internal/session/room/RoomFactory.kt | 4 +- .../session/room/members/RoomMembers.kt | 12 +++++ ...ractor.kt => SenderRoomMemberExtractor.kt} | 45 +++++++++---------- .../session/room/timeline/DefaultTimeline.kt | 30 ++++++++++--- .../room/timeline/TimelineEventFactory.kt | 25 +++++++++-- .../timeline/TimelineEventController.kt | 42 ++++++++++++----- .../timeline/factory/CallItemFactory.kt | 17 ++++--- .../timeline/factory/MessageItemFactory.kt | 9 ++-- .../RoomHistoryVisibilityItemFactory.kt | 11 +++-- .../timeline/factory/RoomMemberItemFactory.kt | 30 +++++++------ .../timeline/factory/RoomNameItemFactory.kt | 14 +++--- .../timeline/factory/RoomTopicItemFactory.kt | 14 +++--- .../EndlessRecyclerViewScrollListener.kt | 18 ++------ .../timeline/helper/RoomMemberEventHelper.kt | 40 +++++++++++++++++ 17 files changed, 210 insertions(+), 128 deletions(-) rename matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/{RoomMemberExtractor.kt => SenderRoomMemberExtractor.kt} (56%) create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index a80e92a0..c43ff438 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -20,7 +20,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest 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.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -57,7 +57,7 @@ internal class TimelineTest : InstrumentedTest { val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) - val roomMemberExtractor = RoomMemberExtractor(ROOM_ID) + val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) return DefaultTimeline(ROOM_ID, initialEventId, monarchy.realmConfiguration, taskExecutor, getContextOfEventTask, timelineEventFactory, paginationTask, null) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt index ca971b65..58044278 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/TimelineEvent.kt @@ -29,7 +29,8 @@ data class TimelineEvent( val root: Event, val localId: String, val displayIndex: Int, - val roomMember: RoomMember?, + val senderName: String?, + val senderAvatar: String?, val sendState: SendState ) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index 6df95e10..28470455 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -56,14 +56,10 @@ internal fun ChunkEntity.merge(roomId: String, if (direction == PaginationDirection.FORWARDS) { this.nextToken = chunkToMerge.nextToken this.isLastForward = chunkToMerge.isLastForward - this.forwardsStateIndex = chunkToMerge.forwardsStateIndex - this.forwardsDisplayIndex = chunkToMerge.forwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) } else { this.prevToken = chunkToMerge.prevToken this.isLastBackward = chunkToMerge.isLastBackward - this.backwardsStateIndex = chunkToMerge.backwardsStateIndex - this.backwardsDisplayIndex = chunkToMerge.backwardsDisplayIndex eventsToMerge = chunkToMerge.events.sort(EventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) } eventsToMerge.forEach { @@ -119,20 +115,20 @@ internal fun ChunkEntity.add(roomId: String, this.displayIndex = currentDisplayIndex this.sendState = SendState.SYNCED } - // We are not using the order of the list, but will be sorting with displayIndex field - events.add(eventEntity) + val position = if (direction == PaginationDirection.FORWARDS) 0 else this.events.size + events.add(position, eventEntity) } internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsDisplayIndex - PaginationDirection.BACKWARDS -> backwardsDisplayIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsDisplayIndex + PaginationDirection.BACKWARDS -> backwardsDisplayIndex + } ?: defaultValue } internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int { return when (direction) { - PaginationDirection.FORWARDS -> forwardsStateIndex - PaginationDirection.BACKWARDS -> backwardsStateIndex - } ?: defaultValue + PaginationDirection.FORWARDS -> forwardsStateIndex + PaginationDirection.BACKWARDS -> backwardsStateIndex + } ?: defaultValue } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 146794e1..24e51355 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -21,7 +21,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.session.room.invite.InviteTask import im.vector.matrix.android.internal.session.room.members.DefaultRoomMembersService import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.DefaultSendService @@ -45,7 +45,7 @@ internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, private val taskExecutor: TaskExecutor) { fun instantiate(roomId: String): Room { - val roomMemberExtractor = RoomMemberExtractor(roomId) + val roomMemberExtractor = SenderRoomMemberExtractor(roomId) val timelineEventFactory = TimelineEventFactory(roomMemberExtractor) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, contextOfEventTask, timelineEventFactory, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 6c6d78ed..40266cb7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -48,6 +48,18 @@ internal class RoomMembers(private val realm: Realm, } } + fun isUniqueDisplayName(displayName: String?): Boolean { + if(displayName.isNullOrEmpty()){ + return true + } + return EventEntity + .where(realm, roomId, EventType.STATE_ROOM_MEMBER) + .contains(EventEntityFields.CONTENT, displayName) + .distinct(EventEntityFields.STATE_KEY) + .findAll() + .size == 1 + } + fun queryRoomMembersEvent(): RealmQuery { return EventEntity .where(realm, roomId, EventType.STATE_ROOM_MEMBER) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt similarity index 56% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt index f2ec6a05..d07175d4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMemberExtractor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/SenderRoomMemberExtractor.kt @@ -20,48 +20,45 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.next import im.vector.matrix.android.internal.database.query.prev import im.vector.matrix.android.internal.database.query.where -import io.realm.Realm +import io.realm.RealmList import io.realm.RealmQuery -internal class RoomMemberExtractor(private val roomId: String) { - - private val cached = HashMap() +internal class SenderRoomMemberExtractor(private val roomId: String) { fun extractFrom(event: EventEntity): RoomMember? { val sender = event.sender ?: return null - val cacheKey = sender + event.stateIndex - if (cached.containsKey(cacheKey)) { - return cached[cacheKey] - } // If the event is unlinked we want to fetch unlinked state events val unlinked = event.isUnlinked - // When stateIndex is negative, we try to get the next stateEvent prevContent() - // If prevContent is null we fallback to the Int.MIN state events content() - val content = if (event.stateIndex <= 0) { - baseQuery(event.realm, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent - ?: baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content - } else { - baseQuery(event.realm, roomId, sender, unlinked).prev(since = event.stateIndex)?.content + val roomEntity = RoomEntity.where(event.realm, roomId = roomId).findFirst() ?: return null + val chunkEntity = ChunkEntity.findIncludingEvent(event.realm, event.eventId) + val content = when { + chunkEntity == null -> null + event.stateIndex <= 0 -> baseQuery(chunkEntity.events, sender, unlinked).next(from = event.stateIndex)?.prevContent + else -> baseQuery(chunkEntity.events, sender, unlinked).prev(since = event.stateIndex)?.content } - val roomMember: RoomMember? = ContentMapper.map(content).toModel() - cached[cacheKey] = roomMember - return roomMember + + val fallbackContent = content + ?: baseQuery(roomEntity.untimelinedStateEvents, sender, unlinked).prev(since = event.stateIndex)?.content + + return ContentMapper.map(fallbackContent).toModel() } - private fun baseQuery(realm: Realm, - roomId: String, + private fun baseQuery(list: RealmList, sender: String, isUnlinked: Boolean): RealmQuery { - - val filterMode = if (isUnlinked) EventEntity.LinkFilterMode.UNLINKED_ONLY else EventEntity.LinkFilterMode.LINKED_ONLY - return EventEntity - .where(realm, roomId = roomId, type = EventType.STATE_ROOM_MEMBER, linkFilterMode = filterMode) + return list + .where() .equalTo(EventEntityFields.STATE_KEY, sender) + .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER) + .equalTo(EventEntityFields.IS_UNLINKED, isUnlinked) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 89a8307b..58274288 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -27,14 +27,24 @@ 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.util.CancelableBag import im.vector.matrix.android.api.util.addTo -import im.vector.matrix.android.internal.database.model.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.ChunkEntityFields +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.EventEntityFields +import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.query.findIncludingEvent import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer -import io.realm.* +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import timber.log.Timber import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -87,9 +97,16 @@ internal class DefaultTimeline( private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> - if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { + if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL ) { handleInitialLoad() } else { + // If changeSet has deletion we are having a gap, so we clear everything + if(changeSet.deletionRanges.isNotEmpty()){ + prevDisplayIndex = DISPLAY_INDEX_UNKNOWN + nextDisplayIndex = DISPLAY_INDEX_UNKNOWN + builtEvents.clear() + timelineEventFactory.clear() + } changeSet.insertionRanges.forEach { range -> val (startDisplayIndex, direction) = if (range.startIndex == 0) { Pair(liveEvents[range.length - 1]!!.displayIndex, Timeline.Direction.FORWARDS) @@ -108,6 +125,7 @@ internal class DefaultTimeline( buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } + } } } @@ -298,9 +316,9 @@ internal class DefaultTimeline( private fun executePaginationTask(direction: Timeline.Direction, limit: Int) { val token = getTokenLive(direction) ?: return val params = PaginationTask.Params(roomId = roomId, - from = token, - direction = direction.toPaginationDirection(), - limit = limit) + from = token, + direction = direction.toPaginationDirection(), + limit = limit) Timber.v("Should fetch $limit items $direction") paginationTask.configureWith(params) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt index 20c0ebee..28cd2a9d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineEventFactory.kt @@ -19,19 +19,36 @@ package im.vector.matrix.android.internal.session.room.timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor -internal class TimelineEventFactory(private val roomMemberExtractor: RoomMemberExtractor) { +internal class TimelineEventFactory(private val roomMemberExtractor: SenderRoomMemberExtractor) { + + private val cached = mutableMapOf() fun create(eventEntity: EventEntity): TimelineEvent { - val roomMember = roomMemberExtractor.extractFrom(eventEntity) + val sender = eventEntity.sender + val cacheKey = sender + eventEntity.stateIndex + val senderData = cached.getOrPut(cacheKey) { + val senderRoomMember = roomMemberExtractor.extractFrom(eventEntity) + SenderData(senderRoomMember?.displayName, senderRoomMember?.avatarUrl) + } return TimelineEvent( eventEntity.asDomain(), eventEntity.localId, eventEntity.displayIndex, - roomMember, + senderData.senderName, + senderData.senderAvatar, eventEntity.sendState ) } + fun clear(){ + cached.clear() + } + + private data class SenderData( + val senderName: String?, + val senderAvatar: String? + ) + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 39240dce..05b20b2f 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -24,6 +24,8 @@ import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyModel +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent @@ -33,7 +35,15 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.core.epoxy.LoadingItemModel_ 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.helper.* +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper +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.TimelineEventVisibilityStateChangedListener +import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.canBeMerged +import im.vector.riotredesign.features.home.room.detail.timeline.helper.nextDisplayableEvent +import im.vector.riotredesign.features.home.room.detail.timeline.helper.prevSameTypeEvents 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.home.room.detail.timeline.item.MergedHeaderItem @@ -160,8 +170,8 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, // Should be build if not cached or if cached but contains mergedHeader or formattedDay // We then are sure we always have items up to date. if (modelCache[position] == null - || modelCache[position]?.mergedHeaderModel != null - || modelCache[position]?.formattedDayModel != null) { + || modelCache[position]?.mergedHeaderModel != null + || modelCache[position]?.formattedDayModel != null) { modelCache[position] = buildItemModels(position, currentSnapshot) } } @@ -217,24 +227,32 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter, if (prevSameTypeEvents.isEmpty()) { null } else { - val mergedEvents = (listOf(event) + prevSameTypeEvents) - val mergedData = mergedEvents.map { - val roomMember = event.roomMember + val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() + val mergedData = mergedEvents.map { mergedEvent -> + val eventContent: RoomMember? = mergedEvent.root.content.toModel() + val prevEventContent: RoomMember? = mergedEvent.root.prevContent.toModel() + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, mergedEvent) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, mergedEvent) MergedHeaderItem.Data( - userId = event.root.sender ?: "", - avatarUrl = roomMember?.avatarUrl, - memberName = roomMember?.displayName ?: "", - eventId = it.localId + userId = mergedEvent.root.sender ?: "", + avatarUrl = senderAvatar, + memberName = senderName ?: "", + eventId = mergedEvent.localId ) } val mergedEventIds = mergedEvents.map { it.localId } - val mergeId = mergedEventIds.joinToString(separator = "_") { it } - val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { true } + // We try to find if one of the item id were used as mergeItemCollapseStates key + // => handle case where paginating from mergeable events and we get more + val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() + val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) + ?: true + val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } if (isCollapsed) { collapsedEventIds.addAll(mergedEventIds) } else { collapsedEventIds.removeAll(mergedEventIds) } + val mergeId = mergedEventIds.joinToString(separator = "_") { it } MergedHeaderItem(isCollapsed, mergeId, mergedData) { mergeItemCollapseStates[event.localId] = it requestModelBuild() diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt index e83183ef..42f6ba0e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -30,27 +30,26 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class CallItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val text = buildNoticeText(event.root, roomMember) ?: return null + val text = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { return when { EventType.CALL_INVITE == event.type -> { val content = event.content.toModel() ?: return null val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO return if (isVideoCall) { - stringProvider.getString(R.string.notice_placed_video_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_video_call, senderName) } else { - stringProvider.getString(R.string.notice_placed_voice_call, roomMember.displayName) + stringProvider.getString(R.string.notice_placed_voice_call, senderName) } } - EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, roomMember.displayName) - EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, roomMember.displayName) + EventType.CALL_ANSWER == event.type -> stringProvider.getString(R.string.notice_answered_call, senderName) + EventType.CALL_HANGUP == event.type -> stringProvider.getString(R.string.notice_ended_call, senderName) else -> null } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 73be1cf2..f17cc295 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -65,8 +65,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ): VectorEpoxyModel<*>? { val eventId = event.root.eventId ?: return null - val roomMember = event.roomMember - val nextRoomMember = nextEvent?.roomMember val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -75,14 +73,15 @@ class MessageItemFactory(private val colorProvider: ColorProvider, ?: false val showInformation = addDaySeparator - || nextRoomMember != roomMember + || event.senderAvatar != nextEvent?.senderAvatar + || event.senderName != nextEvent?.senderName || nextEvent?.root?.type != EventType.MESSAGE || isNextMessageReceivedMoreThanOneHourAgo val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) - val avatarUrl = roomMember?.avatarUrl - val memberName = roomMember?.displayName ?: event.root.sender ?: "" + val avatarUrl = event.senderAvatar + val memberName = event.senderName ?: event.root.sender ?: "" val formattedMemberName = span(memberName) { textColor = colorProvider.getColor(getColorFor(event.root.sender ?: "")) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt index cd87e7bd..3a3a91f1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomHistoryVisibilityItemFactory.kt @@ -31,15 +31,14 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildNoticeText(event.root, roomMember) ?: return null + val noticeText = buildNoticeText(event.root, event.senderName) ?: return null return NoticeItem_() .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } - private fun buildNoticeText(event: Event, roomMember: RoomMember): CharSequence? { + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { val content = event.content.toModel() ?: return null val formattedVisibility = when (content.historyVisibility) { RoomHistoryVisibility.SHARED -> stringProvider.getString(R.string.notice_room_visibility_shared) @@ -47,7 +46,7 @@ class RoomHistoryVisibilityItemFactory(private val stringProvider: StringProvide RoomHistoryVisibility.JOINED -> stringProvider.getString(R.string.notice_room_visibility_joined) RoomHistoryVisibility.WORLD_READABLE -> stringProvider.getString(R.string.notice_room_visibility_world_readable) } - return stringProvider.getString(R.string.notice_made_future_room_visibility, roomMember.displayName, formattedVisibility) + return stringProvider.getString(R.string.notice_made_future_room_visibility, senderName, formattedVisibility) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt index 5d2efcba..77e5920c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomMemberItemFactory.kt @@ -23,6 +23,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.helper.RoomMemberEventHelper import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ @@ -31,18 +32,20 @@ import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem class RoomMemberItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val roomMember = event.roomMember ?: return null - val noticeText = buildRoomMemberNotice(event) ?: return null + val eventContent: RoomMember? = event.root.content.toModel() + val prevEventContent: RoomMember? = event.root.prevContent.toModel() + val noticeText = buildRoomMemberNotice(event, eventContent, prevEventContent) ?: return null + val senderAvatar = RoomMemberEventHelper.senderAvatar(eventContent, prevEventContent, event) + val senderName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + return NoticeItem_() .userId(event.root.sender ?: "") .noticeText(noticeText) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(senderAvatar) + .memberName(senderName) } - private fun buildRoomMemberNotice(event: TimelineEvent): String? { - val eventContent: RoomMember? = event.root.content.toModel() - val prevEventContent: RoomMember? = event.root.prevContent.toModel() + private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { val isMembershipEvent = prevEventContent?.membership != eventContent?.membership return if (isMembershipEvent) { buildMembershipNotice(event, eventContent, prevEventContent) @@ -62,7 +65,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName) else -> stringProvider.getString(R.string.notice_display_name_changed_from, - event.root.sender, prevEventContent?.displayName, eventContent?.displayName) + event.root.sender, prevEventContent?.displayName, eventContent?.displayName) } displayText.append(displayNameText) } @@ -72,7 +75,7 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { displayText.append(" ") stringProvider.getString(R.string.notice_avatar_changed_too) } else { - stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName) + stringProvider.getString(R.string.notice_avatar_url_changed, event.senderName) } displayText.append(displayAvatarText) } @@ -80,16 +83,16 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { } private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? { - val senderDisplayName = event.roomMember?.displayName ?: return null + val senderDisplayName = event.senderName ?: event.root.sender val targetDisplayName = eventContent?.displayName ?: event.root.sender return when { Membership.INVITE == eventContent?.membership -> { // TODO get userId - val selfUserId: String = "" + val selfUserId = "" when { eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, - targetDisplayName, eventContent.thirdPartyInvite?.displayName) + targetDisplayName, eventContent.thirdPartyInvite?.displayName) TextUtils.equals(event.root.stateKey, selfUserId) -> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName) event.root.stateKey.isNullOrEmpty() -> @@ -106,7 +109,8 @@ class RoomMemberItemFactory(private val stringProvider: StringProvider) { if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_reject, senderDisplayName) } else { - stringProvider.getString(R.string.notice_room_leave, senderDisplayName) + val leftDisplayName = RoomMemberEventHelper.senderName(eventContent, prevEventContent, event) + stringProvider.getString(R.string.notice_room_leave, leftDisplayName) } } else if (prevEventContent?.membership == Membership.INVITE) { stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt index 3904d7c7..be33c44e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomNameItemFactory.kt @@ -29,20 +29,16 @@ class RoomNameItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomNameContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomNameContent = event.root.content.toModel() ?: return null val text = if (!TextUtils.isEmpty(content.name)) { - stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name) + stringProvider.getString(R.string.notice_room_name_changed, event.senderName, content.name) } else { - stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_name_removed, event.senderName) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt index 5aa31c9b..34e55897 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/RoomTopicItemFactory.kt @@ -28,20 +28,16 @@ class RoomTopicItemFactory(private val stringProvider: StringProvider) { fun create(event: TimelineEvent): NoticeItem? { - val content: RoomTopicContent? = event.root.content.toModel() - val roomMember = event.roomMember - if (content == null || roomMember == null) { - return null - } + val content: RoomTopicContent = event.root.content.toModel() ?: return null val text = if (content.topic.isNullOrEmpty()) { - stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName) + stringProvider.getString(R.string.notice_room_topic_removed, event.senderName) } else { - stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic) + stringProvider.getString(R.string.notice_room_topic_changed, event.senderName, content.topic) } return NoticeItem_() .noticeText(text) - .avatarUrl(roomMember.avatarUrl) - .memberName(roomMember.displayName) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt index c88bd7e9..d078e72c 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/EndlessRecyclerViewScrollListener.kt @@ -34,28 +34,18 @@ class EndlessRecyclerViewScrollListener(private val layoutManager: LinearLayoutM // This happens many times a second during a scroll, so be wary of the code you place here. // We are given a few useful parameters to help us work out if we need to load some more data, // but first we check if we are waiting for the previous load to finish. + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() val totalItemCount = layoutManager.itemCount - // The minimum amount of items to have below your current scroll position - // before loading more. - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loadingForwards = true - loadingBackwards = true - } - } - // If it’s still loading, we check to see if the dataset count has + // We check to see if the dataset count has // changed, if so we conclude it has finished loading - if (totalItemCount > previousTotalItemCount) { + if (totalItemCount != previousTotalItemCount) { + previousTotalItemCount = totalItemCount loadingBackwards = false loadingForwards = false - previousTotalItemCount = totalItemCount } // If it isn’t currently loading, we check to see if we have reached // the visibleThreshold and need to reload more data. diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt new file mode 100644 index 00000000..499f92f7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/helper/RoomMemberEventHelper.kt @@ -0,0 +1,40 @@ +/* + * 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. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.helper + +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.api.session.room.model.RoomMember +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + +object RoomMemberEventHelper { + + fun senderAvatar(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.avatarUrl == null && prevEventContent?.avatarUrl != null) { + prevEventContent.avatarUrl + } else { + event.senderAvatar + } + } + + fun senderName(eventContent: RoomMember?, prevEventContent: RoomMember?, event: TimelineEvent): String? { + return if (eventContent?.membership == Membership.LEAVE && eventContent.displayName == null && prevEventContent?.displayName != null) { + prevEventContent.displayName + } else { + event.senderName + } + } +} \ No newline at end of file