Timeline : start handling some state events display

This commit is contained in:
ganfra
2019-01-15 15:47:03 +01:00
parent 5e89627867
commit 06dd3760c5
47 changed files with 346 additions and 50 deletions

View File

@ -3,6 +3,7 @@ package im.vector.riotredesign.core.di
import android.content.Context
import android.content.Context.MODE_PRIVATE
import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import org.koin.dsl.module.module
@ -14,6 +15,10 @@ class AppModule(private val context: Context) {
LocaleProvider(context.resources)
}
single {
StringProvider(context.resources)
}
single {
context.getSharedPreferences("im.vector.riot", MODE_PRIVATE)
}

View File

@ -0,0 +1,39 @@
package im.vector.riotredesign.core.resources
import android.content.res.Resources
import android.support.annotation.NonNull
import android.support.annotation.StringRes
class StringProvider(private val resources: Resources) {
/**
* Returns a localized string from the application's package's
* default string table.
*
* @param resId Resource id for the string
* @return The string data associated with the resource, stripped of styled
* text information.
*/
@NonNull
fun getString(@StringRes resId: Int): String {
return resources.getString(resId)
}
/**
* Returns a localized formatted string from the application's package's
* default string table, substituting the format arguments as defined in
* [java.util.Formatter] and [java.lang.String.format].
*
* @param resId Resource id for the format string
* @param formatArgs The format arguments that will be used for
* substitution.
* @return The string data associated with the resource, formatted and
* stripped of styled text information.
*/
@NonNull
fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
return resources.getString(resId, *formatArgs)
}
}

View File

@ -29,7 +29,7 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
private val homeNavigator by inject<HomeNavigator>()
override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule(this).definition))
loadKoinModules(listOf(HomeModule().definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)

View File

@ -2,13 +2,17 @@ package im.vector.riotredesign.features.home
import im.vector.riotredesign.features.home.group.SelectedGroupHolder
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TextItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomMemberItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomNameItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomTopicItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
import org.koin.dsl.module.module
class HomeModule(private val homeActivity: HomeActivity) {
class HomeModule {
val definition = module(override = true) {
@ -21,7 +25,23 @@ class HomeModule(private val homeActivity: HomeActivity) {
}
single {
TextItemFactory()
RoomNameItemFactory(get())
}
single {
RoomTopicItemFactory(get())
}
single {
RoomMemberItemFactory(get())
}
single {
DefaultItemFactory()
}
single {
TimelineItemFactory(get(), get(), get(), get(), get())
}
single {
@ -29,7 +49,7 @@ class HomeModule(private val homeActivity: HomeActivity) {
}
factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get())
TimelineEventController(roomId, get(), get())
}
single {

View File

@ -4,7 +4,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
class BlankItem
: KotlinModel(R.layout.item_event_blank) {
: KotlinModel(R.layout.item_timeline_event_blank) {
override fun bind() {
//no-op

View File

@ -6,7 +6,7 @@ import im.vector.riotredesign.core.epoxy.KotlinModel
data class DaySeparatorItem(
val formattedDay: CharSequence
) : KotlinModel(R.layout.item_event_day_separator) {
) : KotlinModel(R.layout.item_timeline_event_day_separator) {
private val dayTextView by bind<TextView>(R.id.itemDayTextView)

View File

@ -4,9 +4,9 @@ import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
class TextItem(
class DefaultItem(
val text: CharSequence? = null
) : KotlinModel(R.layout.item_event_text) {
) : KotlinModel(R.layout.item_timeline_event_default) {
private val messageView by bind<TextView>(R.id.stateMessageView)

View File

@ -2,11 +2,11 @@ package im.vector.riotredesign.features.home.room.detail.timeline
import im.vector.matrix.android.api.session.events.model.TimelineEvent
class TextItemFactory {
class DefaultItemFactory {
fun create(event: TimelineEvent): TextItem? {
fun create(event: TimelineEvent): DefaultItem? {
val text = "${event.root.type} events are not yet handled"
return TextItem(text = text)
return DefaultItem(text = text)
}
}

View File

@ -14,7 +14,7 @@ class MessageItem(
val avatarUrl: String?,
val memberName: CharSequence? = null,
val showInformation: Boolean = true
) : KotlinModel(R.layout.item_event_message) {
) : KotlinModel(R.layout.item_timeline_event_message) {
private val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
private val memberNameView by bind<TextView>(R.id.messageMemberNameView)

View File

@ -0,0 +1,21 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import android.widget.ImageView
import android.widget.TextView
import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.features.home.AvatarRenderer
class NoticeItem(private val noticeText: CharSequence? = null,
private val avatarUrl: String?,
private val memberName: CharSequence? = null)
: KotlinModel(R.layout.item_timeline_event_notice) {
private val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
private val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
override fun bind() {
noticeTextView.text = noticeText
AvatarRenderer.render(avatarUrl, memberName?.toString(), avatarImageView)
}
}

View File

@ -0,0 +1,97 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
//TODO : complete with call membership events
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, roomMember.avatarUrl, roomMember.displayName)
}
private fun buildRoomMemberNotice(event: TimelineEvent): String? {
val eventContent: RoomMember? = event.root.content.toModel()
val prevEventContent: RoomMember? = event.root.prevContent.toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
return if (isMembershipEvent) {
buildMembershipNotice(event, eventContent, prevEventContent)
} else {
buildProfileNotice(event, eventContent, prevEventContent)
}
}
private fun buildProfileNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val displayText = StringBuilder()
// Check display name has been changed
if (!TextUtils.equals(eventContent?.displayName, prevEventContent?.displayName)) {
val displayNameText = when {
prevEventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_set, event.root.sender, eventContent?.displayName)
eventContent?.displayName.isNullOrEmpty() -> stringProvider.getString(R.string.notice_display_name_removed, event.root.sender, prevEventContent?.displayName)
else -> stringProvider.getString(R.string.notice_display_name_changed_from, event.root.sender, prevEventContent?.displayName, eventContent?.displayName)
}
displayText.append(displayNameText)
}
// Check whether the avatar has been changed
if (!TextUtils.equals(eventContent?.avatarUrl, prevEventContent?.avatarUrl)) {
val displayAvatarText = if (displayText.isNotEmpty()) {
displayText.append(" ")
stringProvider.getString(R.string.notice_avatar_changed_too)
} else {
stringProvider.getString(R.string.notice_avatar_url_changed, event.roomMember?.displayName)
}
displayText.append(displayAvatarText)
}
return displayText.toString()
}
private fun buildMembershipNotice(event: TimelineEvent, eventContent: RoomMember?, prevEventContent: RoomMember?): String? {
val senderDisplayName = event.roomMember?.displayName ?: return null
val targetDisplayName = eventContent?.displayName ?: event.root.sender
return when {
Membership.INVITE == eventContent?.membership -> {
// TODO get userId
val selfUserId: String = ""
when {
eventContent.thirdPartyInvite != null -> stringProvider.getString(R.string.notice_room_third_party_registered_invite, targetDisplayName, eventContent.thirdPartyInvite?.displayName)
TextUtils.equals(event.root.stateKey, selfUserId)
-> stringProvider.getString(R.string.notice_room_invite_you, senderDisplayName)
event.root.stateKey.isNullOrEmpty() -> stringProvider.getString(R.string.notice_room_invite_no_invitee, senderDisplayName)
else -> stringProvider.getString(R.string.notice_room_invite, senderDisplayName, targetDisplayName)
}
}
Membership.JOIN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_join, senderDisplayName)
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)
}
} else if (prevEventContent?.membership == Membership.INVITE) {
stringProvider.getString(R.string.notice_room_withdraw, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.JOIN) {
stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
} else if (prevEventContent?.membership == Membership.BAN) {
stringProvider.getString(R.string.notice_room_unban, senderDisplayName, targetDisplayName)
} else {
null
}
Membership.BAN == eventContent?.membership -> stringProvider.getString(R.string.notice_room_ban, senderDisplayName, targetDisplayName)
Membership.KNOCK == eventContent?.membership -> stringProvider.getString(R.string.notice_room_kick, senderDisplayName, targetDisplayName)
else -> null
}
}
}

View File

@ -0,0 +1,28 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomNameContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
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 text = if (!TextUtils.isEmpty(content.name)) {
stringProvider.getString(R.string.notice_room_name_changed, roomMember.displayName, content.name)
} else {
stringProvider.getString(R.string.notice_room_name_removed, roomMember.displayName)
}
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
}
}

View File

@ -0,0 +1,28 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import android.text.TextUtils
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider
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 text = if (!TextUtils.isEmpty(content.topic)) {
stringProvider.getString(R.string.notice_room_topic_changed, roomMember.displayName, content.topic)
} else {
stringProvider.getString(R.string.notice_room_topic_removed, roomMember.displayName)
}
return NoticeItem(text, roomMember.avatarUrl, roomMember.displayName)
}
}

View File

@ -2,17 +2,16 @@ package im.vector.riotredesign.features.home.room.detail.timeline
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController
class TimelineEventController(private val roomId: String,
private val messageItemFactory: MessageItemFactory,
private val textItemFactory: TextItemFactory,
private val dateFormatter: TimelineDateFormatter
private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory
) : PagedListEpoxyController<TimelineEvent>(
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler()
@ -50,15 +49,10 @@ class TimelineEventController(private val roomId: String,
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val item = when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
else -> textItemFactory.create(event)
}
item?.also {
timelineItemFactory.create(event, nextEvent, addDaySeparator, date, callback)?.also {
it.id(event.localId)
epoxyModels.add(it)
}
if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem(formattedDay).id(roomId + formattedDay)

View File

@ -0,0 +1,29 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.TimelineEvent
import org.threeten.bp.LocalDateTime
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory,
private val roomTopicItemFactory: RoomTopicItemFactory,
private val roomMemberItemFactory: RoomMemberItemFactory,
private val defaultItemFactory: DefaultItemFactory) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
addDaySeparator: Boolean,
date: LocalDateTime,
callback: TimelineEventController.Callback?): EpoxyModel<*>? {
return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, addDaySeparator, date, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
else -> defaultItemFactory.create(event)
}
}
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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">
<ImageView
android:id="@+id/itemNoticeAvatarView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/itemNoticeTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:textColor="@color/slate_grey"
android:textSize="14sp"
android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/itemNoticeAvatarView"
app:layout_constraintTop_toTopOf="parent"
tools:text="Mon item" />
</android.support.constraint.ConstraintLayout>