forked from GitHub-Mirror/riotX-android
Merge pull request #109 from vector-im/feature/timeline_formatting
This commit is contained in:
@ -29,7 +29,7 @@
|
||||
|
||||
<activity android:name=".features.home.HomeActivity" />
|
||||
<activity android:name=".features.login.LoginActivity" />
|
||||
<activity android:name=".features.media.MediaViewerActivity" />
|
||||
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
||||
<activity
|
||||
android:name=".features.rageshake.BugReportActivity"
|
||||
android:label="@string/title_activity_bug_report" />
|
||||
@ -37,6 +37,7 @@
|
||||
android:name=".features.settings.VectorSettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity android:name=".features.media.VideoMediaViewerActivity" />
|
||||
|
||||
<service
|
||||
android:name=".core.services.CallService"
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
@ -31,6 +31,7 @@ class AutocompleteUserController : TypedEpoxyController<List<User>>() {
|
||||
data.forEach { user ->
|
||||
autocompleteUserItem {
|
||||
id(user.userId)
|
||||
userId(user.userId)
|
||||
name(user.displayName)
|
||||
avatarUrl(user.avatarUrl)
|
||||
clickListener { _ ->
|
||||
|
@ -29,18 +29,15 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
@EpoxyModelClass(layout = R.layout.item_autocomplete_user)
|
||||
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
|
||||
|
||||
@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() {
|
||||
|
@ -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<Drawable>) {
|
||||
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<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession!!.contentUrlResolver()
|
||||
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
@ -35,6 +35,7 @@ class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
||||
val isSelected = groupSummary.groupId == selected?.groupId
|
||||
groupSummaryItem {
|
||||
id(groupSummary.groupId)
|
||||
groupId(groupSummary.groupId)
|
||||
groupName(groupSummary.displayName)
|
||||
selected(isSelected)
|
||||
avatarUrl(groupSummary.avatarUrl)
|
||||
|
@ -29,6 +29,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
|
||||
|
||||
@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<GroupSummaryItem.Holder>() {
|
||||
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() {
|
||||
|
@ -37,6 +37,10 @@ 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
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.riotredesign.R
|
||||
@ -60,8 +64,10 @@ 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.MediaViewerActivity
|
||||
import im.vector.riotredesign.features.media.ImageContentRenderer
|
||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -367,7 +373,7 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
.show()
|
||||
}
|
||||
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
// TimelineEventController.Callback ************************************************************
|
||||
|
||||
override fun onUrlClicked(url: String) {
|
||||
homePermalinkHandler.launch(url)
|
||||
@ -377,12 +383,25 @@ class RoomDetailFragment : VectorBaseFragment(), TimelineEventController.Callbac
|
||||
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event))
|
||||
}
|
||||
|
||||
override fun onMediaClicked(mediaData: MediaContentRenderer.Data, view: View) {
|
||||
val intent = MediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// AutocompleteUserPresenter.Callback
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onFileMessageClicked(messageFileContent: MessageFileContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
}
|
||||
|
||||
override fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) {
|
||||
vectorBaseActivity.notImplemented()
|
||||
}
|
||||
|
||||
// AutocompleteUserPresenter.Callback
|
||||
|
||||
override fun onQueryUsers(query: CharSequence?) {
|
||||
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
|
||||
|
@ -24,16 +24,32 @@ 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.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
|
||||
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.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.media.MediaContentRenderer
|
||||
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,
|
||||
@ -44,10 +60,16 @@ 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: VideoContentRenderer.Data, view: View)
|
||||
fun onFileMessageClicked(messageFileContent: MessageFileContent)
|
||||
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
|
||||
}
|
||||
|
||||
private val modelCache = arrayListOf<List<EpoxyModel<*>>>()
|
||||
private val collapsedEventIds = linkedSetOf<String>()
|
||||
private val mergeItemCollapseStates = HashMap<String, Boolean>()
|
||||
private val modelCache = arrayListOf<CacheItemData?>()
|
||||
|
||||
private var currentSnapshot: List<TimelineEvent> = emptyList()
|
||||
private var inSubmitList: Boolean = false
|
||||
private var timeline: Timeline? = null
|
||||
@ -60,7 +82,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()
|
||||
}
|
||||
@ -76,11 +98,8 @@ 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()
|
||||
}
|
||||
(0 until count).forEach {
|
||||
modelCache.add(position, emptyList())
|
||||
modelCache.add(position, null)
|
||||
}
|
||||
requestModelBuild()
|
||||
}
|
||||
@ -116,7 +135,6 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
.id("forward_loading_item")
|
||||
.addWhen(Timeline.Direction.FORWARDS)
|
||||
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
|
||||
@ -149,53 +167,110 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
@Synchronized
|
||||
private fun getModels(): List<EpoxyModel<*>> {
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
if (modelCache[position].isEmpty()) {
|
||||
// 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.flatten()
|
||||
return modelCache
|
||||
.map {
|
||||
val eventModel = if (it == null || collapsedEventIds.contains(it.localId)) {
|
||||
null
|
||||
} else {
|
||||
it.eventModel
|
||||
}
|
||||
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel)
|
||||
}
|
||||
.flatten()
|
||||
.filterNotNull()
|
||||
}
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): List<EpoxyModel<*>> {
|
||||
val epoxyModels = ArrayList<EpoxyModel<*>>()
|
||||
|
||||
private fun buildItemModels(currentPosition: Int, items: List<TimelineEvent>): CacheItemData {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextDisplayableEvent(currentPosition)
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
|
||||
timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
val eventModel = timelineItemFactory.create(event, nextEvent, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
epoxyModels.add(it)
|
||||
}
|
||||
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)
|
||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
epoxyModels.add(daySeparatorItem)
|
||||
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMergedHeaderItem(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
items: List<TimelineEvent>,
|
||||
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 = (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 = mergedEvent.root.sender ?: "",
|
||||
avatarUrl = senderAvatar,
|
||||
memberName = senderName ?: "",
|
||||
eventId = mergedEvent.localId
|
||||
)
|
||||
}
|
||||
val mergedEventIds = mergedEvents.map { it.localId }
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
return epoxyModels
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
private data class CacheItemData(
|
||||
val localId: String,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null
|
||||
)
|
||||
|
@ -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<CallInviteContent>() ?: 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
|
||||
}
|
||||
|
||||
|
@ -18,16 +18,19 @@ 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
|
||||
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
|
||||
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
|
||||
@ -39,13 +42,16 @@ 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.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
|
||||
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 im.vector.riotredesign.features.media.VideoContentRenderer
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
@ -59,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()
|
||||
@ -69,56 +73,113 @@ 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 informationData = MessageInformationData(time, avatarUrl, memberName, showInformation)
|
||||
val avatarUrl = event.senderAvatar
|
||||
val memberName = event.senderName ?: event.root.sender ?: ""
|
||||
val formattedMemberName = span(memberName) {
|
||||
textColor = colorProvider.getColor(getColorFor(event.root.sender ?: ""))
|
||||
}
|
||||
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 MessageEmoteContent -> buildEmoteMessageItem(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)
|
||||
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)
|
||||
}
|
||||
|
||||
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 thumbnailData = ImageContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
url = messageContent.info?.thumbnailUrl,
|
||||
height = messageContent.info?.height,
|
||||
maxHeight = maxHeight,
|
||||
width = messageContent.info?.width,
|
||||
maxWidth = maxWidth
|
||||
)
|
||||
|
||||
val videoData = VideoContentRenderer.Data(
|
||||
filename = messageContent.body,
|
||||
videoUrl = messageContent.url,
|
||||
thumbnailMediaData = thumbnailData
|
||||
)
|
||||
|
||||
return MessageImageVideoItem_()
|
||||
.playable(true)
|
||||
.informationData(informationData)
|
||||
.mediaData(thumbnailData)
|
||||
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
|
||||
}
|
||||
|
||||
private fun buildTextMessageItem(messageContent: MessageTextContent,
|
||||
informationData: MessageInformationData,
|
||||
callback: TimelineEventController.Callback?): MessageTextItem? {
|
||||
|
||||
@ -126,15 +187,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)
|
||||
@ -181,4 +234,31 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
||||
return spannable
|
||||
}
|
||||
|
||||
//Based on riot-web implementation
|
||||
@ColorRes
|
||||
private fun getColorFor(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
|
||||
}
|
||||
}
|
||||
}
|
@ -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<RoomHistoryVisibilityContent>() ?: 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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,17 +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
|
||||
return NoticeItem_()
|
||||
.noticeText(noticeText)
|
||||
.avatarUrl(roomMember.avatarUrl)
|
||||
.memberName(roomMember.displayName)
|
||||
}
|
||||
|
||||
private fun buildRoomMemberNotice(event: TimelineEvent): String? {
|
||||
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(senderAvatar)
|
||||
.memberName(senderName)
|
||||
}
|
||||
|
||||
private fun buildRoomMemberNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
|
||||
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|
||||
return if (isMembershipEvent) {
|
||||
buildMembershipNotice(event, eventContent, prevEventContent)
|
||||
@ -57,11 +61,11 @@ 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)
|
||||
event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
|
||||
}
|
||||
displayText.append(displayNameText)
|
||||
}
|
||||
@ -71,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)
|
||||
}
|
||||
@ -79,33 +83,34 @@ 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 ->
|
||||
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() ->
|
||||
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) {
|
||||
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)
|
||||
@ -116,11 +121,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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
@ -57,7 +57,7 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
|
||||
} catch (e: Exception) {
|
||||
defaultItemFactory.create(event, e)
|
||||
}
|
||||
return computedModel ?: EmptyItem_()
|
||||
return (computedModel ?: EmptyItem_())
|
||||
}
|
||||
|
||||
}
|
@ -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<String, ContentUploadStateTracker.UpdateListener>()
|
||||
|
||||
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) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
@ -48,8 +49,48 @@ fun List<TimelineEvent>.filterDisplayableEvents(): List<TimelineEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
return root.type == EventType.STATE_ROOM_MEMBER
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||
if (index >= size - 1) {
|
||||
return emptyList()
|
||||
}
|
||||
val timelineEvent = this[index]
|
||||
val nextSubList = subList(index + 1, size)
|
||||
val indexOfNextDay = nextSubList.indexOfFirst {
|
||||
val date = it.root.localDateTime()
|
||||
val nextDate = timelineEvent.root.localDateTime()
|
||||
date.toLocalDate() != nextDate.toLocalDate()
|
||||
}
|
||||
val nextSameDayEvents = if (indexOfNextDay == -1) {
|
||||
nextSubList
|
||||
} else {
|
||||
nextSubList.subList(0, indexOfNextDay)
|
||||
}
|
||||
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.type != timelineEvent.root.type }
|
||||
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
||||
nextSameDayEvents
|
||||
} else {
|
||||
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
|
||||
}
|
||||
if (sameTypeEvents.size < minSize) {
|
||||
return emptyList()
|
||||
}
|
||||
return sameTypeEvents
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||
val prevSub = subList(0, index + 1)
|
||||
return prevSub
|
||||
.reversed()
|
||||
.nextSameTypeEvents(0, minSize)
|
||||
.reversed()
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.nextDisplayableEvent(index: Int): TimelineEvent? {
|
||||
return if (index == size - 1) {
|
||||
return if (index >= size - 1) {
|
||||
null
|
||||
} else {
|
||||
subList(index + 1, this.size).firstOrNull { it.isDisplayable() }
|
||||
|
@ -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 event: TimelineEvent)
|
||||
: VectorEpoxyModel.OnVisibilityStateChangedListener {
|
||||
|
||||
override fun onVisibilityStateChanged(visibilityState: Int) {
|
||||
if (visibilityState == VisibilityState.VISIBLE) {
|
||||
callback?.onEventVisible(event)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -35,7 +35,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
||||
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<H : AbsMessageItem.Holder> : VectorEpoxyModel<H>()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun View.renderSendState() {
|
||||
isClickable = informationData.sendState.isSent()
|
||||
alpha = if (informationData.sendState.isSent()) 1f else 0.5f
|
||||
}
|
||||
|
||||
abstract class Holder : VectorEpoxyHolder() {
|
||||
abstract val avatarImageView: ImageView
|
||||
abstract val memberNameView: TextView
|
||||
|
@ -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<Data>,
|
||||
private val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
) : VectorEpoxyModel<MergedHeaderItem.Holder>() {
|
||||
|
||||
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<TextView>(R.id.itemMergedExpandTextView)
|
||||
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
||||
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
||||
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
|
||||
|
||||
}
|
||||
}
|
@ -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<MessageFileItem.Holder>() {
|
||||
|
||||
@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<ImageView>(R.id.messageAvatarImageView)
|
||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
|
||||
val filenameView by bind<TextView>(R.id.messageFilenameView)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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<MessageImageItem.Holder>() {
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_image_video_message)
|
||||
abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Holder>() {
|
||||
|
||||
@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<MessageImageItem.Holder>() {
|
||||
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||
override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||
override val timeView by bind<TextView>(R.id.messageTimeView)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageImageUploadProgressLayout)
|
||||
val imageView by bind<ImageView>(R.id.messageImageView)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||
val imageView by bind<ImageView>(R.id.messageThumbnailView)
|
||||
val playContentView by bind<ImageView>(R.id.messageMediaPlayView)
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -45,6 +45,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
TextViewCompat.getTextMetricsParams(holder.messageView),
|
||||
null)
|
||||
holder.messageView.setTextFuture(textFuture)
|
||||
holder.messageView.renderSendState()
|
||||
findPillsAndProcess { it.bind(holder.messageView) }
|
||||
}
|
||||
|
||||
|
@ -30,11 +30,12 @@ abstract class NoticeItem : VectorEpoxyModel<NoticeItem.Holder>() {
|
||||
|
||||
@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() {
|
||||
|
@ -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)
|
||||
|
@ -31,6 +31,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
|
||||
abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
|
||||
|
||||
@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<RoomSummaryItem.Holder>() {
|
||||
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() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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<Int, Int> {
|
@ -25,26 +25,26 @@ 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)
|
||||
val mediaData = intent.getParcelableExtra<MediaContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
||||
setContentView(im.vector.riotredesign.R.layout.activity_image_media_viewer)
|
||||
val mediaData = intent.getParcelableExtra<ImageContentRenderer.Data>(EXTRA_MEDIA_DATA)
|
||||
if (mediaData.url.isNullOrEmpty()) {
|
||||
finish()
|
||||
} else {
|
||||
configureToolbar(mediaViewerToolbar, mediaData)
|
||||
mediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
mediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
MediaContentRenderer.render(mediaData, mediaViewerImageView)
|
||||
configureToolbar(imageMediaViewerToolbar, mediaData)
|
||||
imageMediaViewerImageView.setImageViewFactory(GlideImageViewFactory())
|
||||
imageMediaViewerImageView.setProgressIndicator(ProgressPieIndicator())
|
||||
ImageContentRenderer.render(mediaData, imageMediaViewerImageView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureToolbar(toolbar: Toolbar, mediaData: MediaContentRenderer.Data) {
|
||||
private fun configureToolbar(toolbar: Toolbar, mediaData: ImageContentRenderer.Data) {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
title = mediaData.filename
|
||||
@ -57,8 +57,8 @@ class MediaViewerActivity : VectorBaseActivity() {
|
||||
|
||||
private const val EXTRA_MEDIA_DATA = "EXTRA_MEDIA_DATA"
|
||||
|
||||
fun newIntent(context: Context, mediaData: MediaContentRenderer.Data): Intent {
|
||||
return Intent(context, MediaViewerActivity::class.java).apply {
|
||||
fun newIntent(context: Context, mediaData: ImageContentRenderer.Data): Intent {
|
||||
return Intent(context, ImageMediaViewerActivity::class.java).apply {
|
||||
putExtra(EXTRA_MEDIA_DATA, mediaData)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -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<VideoContentRenderer.Data>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/mediaViewerToolbar"
|
||||
android:id="@+id/imageMediaViewerToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
@ -15,7 +15,7 @@
|
||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<com.github.piasy.biv.view.BigImageView
|
||||
android:id="@+id/mediaViewerImageView"
|
||||
android:id="@+id/imageMediaViewerImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:failureImageInitScaleType="center"
|
48
vector/src/main/res/layout/activity_video_media_viewer.xml
Normal file
48
vector/src/main/res/layout/activity_video_media_viewer.xml
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2019 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/videoMediaViewerToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoMediaViewerThumbnailView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoMediaViewerVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
135
vector/src/main/res/layout/item_timeline_event_file_message.xml
Normal file
135
vector/src/main/res/layout/item_timeline_event_file_message.xml
Normal file
@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2019 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageAvatarImageView"
|
||||
android:layout_width="@dimen/chat_avatar_size"
|
||||
android:layout_height="@dimen/chat_avatar_size"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageMemberNameView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/messageTimeView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textColor="@color/brown_grey"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
|
||||
tools:text="@tools:sample/date/hhmm" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/messageFileLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageFilee2eIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:src="@drawable/e2e_verified"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- the media type -->
|
||||
<ImageView
|
||||
android:id="@+id/messageFileImageView"
|
||||
android:layout_width="@dimen/chat_avatar_size"
|
||||
android:layout_height="@dimen/chat_avatar_size"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:src="@drawable/filetype_image" />
|
||||
|
||||
<!-- the media -->
|
||||
<TextView
|
||||
android:id="@+id/messageFilenameView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/chat_avatar_size"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_weight="1"
|
||||
android:autoLink="none"
|
||||
android:gravity="center_vertical"
|
||||
tools:text="A filename here" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/messageMediaUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageFileLayout"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,4 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2019 New Vector Ltd
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
@ -43,6 +58,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:duplicateParentState="true"
|
||||
android:textColor="@color/brown_grey"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@ -50,7 +66,7 @@
|
||||
tools:text="@tools:sample/date/hhmm" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageImageView"
|
||||
android:id="@+id/messageThumbnailView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="64dp"
|
||||
@ -59,13 +75,28 @@
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginRight="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:duplicateParentState="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageMemberNameView"
|
||||
tools:layout_height="300dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageMediaPlayView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_material_play_circle"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
|
||||
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
|
||||
app:layout_constraintStart_toStartOf="@id/messageThumbnailView"
|
||||
app:layout_constraintTop_toTopOf="@id/messageThumbnailView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/messageImageUploadProgressLayout"
|
||||
android:id="@+id/messageMediaUploadProgressLayout"
|
||||
layout="@layout/media_upload_download_progress_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="46dp"
|
||||
@ -78,7 +109,7 @@
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageImageView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/messageThumbnailView"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include
|
||||
android:id="@+id/itemMergedAvatarListView"
|
||||
layout="@layout/vector_message_merge_avatar_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemMergedExpandTextView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedExpandTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingRight="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
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="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/itemMergedSeparatorView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?attr/colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="@id/itemMergedExpandTextView"
|
||||
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemMergedExpandTextView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedSummaryTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/itemMergedAvatarListView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemMergedSeparatorView"
|
||||
tools:text="3 membership changes" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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"
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_1"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_2"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_3"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_4"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mels_list_avatar_5"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="-5dp"
|
||||
android:layout_marginLeft="-5dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
</LinearLayout>
|
Reference in New Issue
Block a user