Merge branch 'feature/home_state_issues' into develop

This commit is contained in:
ganfra 2019-03-04 17:00:50 +01:00
commit a146fc43e4
28 changed files with 590 additions and 302 deletions

View file

@ -3,9 +3,14 @@
<words> <words>
<w>connectable</w> <w>connectable</w>
<w>coroutine</w> <w>coroutine</w>
<w>linkify</w>
<w>markon</w>
<w>markwon</w>
<w>merlins</w> <w>merlins</w>
<w>moshi</w> <w>moshi</w>
<w>persistor</w> <w>persistor</w>
<w>restorable</w>
<w>restorables</w>
<w>synchronizer</w> <w>synchronizer</w>
<w>untimelined</w> <w>untimelined</w>
</words> </words>

View file

@ -23,6 +23,7 @@ import com.facebook.stetho.Stetho
import com.jakewharton.threetenabp.AndroidThreeTen import com.jakewharton.threetenabp.AndroidThreeTen
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.riotredesign.core.di.AppModule import im.vector.riotredesign.core.di.AppModule
import im.vector.riotredesign.features.home.HomeModule
import org.koin.log.EmptyLogger import org.koin.log.EmptyLogger
import org.koin.standalone.StandAloneContext.startKoin import org.koin.standalone.StandAloneContext.startKoin
import timber.log.Timber import timber.log.Timber
@ -37,7 +38,9 @@ class Riot : Application() {
Stetho.initializeWithDefaults(this) Stetho.initializeWithDefaults(this)
} }
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
startKoin(listOf(AppModule(this).definition), logger = EmptyLogger()) val appModule = AppModule(applicationContext).definition
val homeModule = HomeModule().definition
startKoin(listOf(appModule, homeModule), logger = EmptyLogger())
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {

View file

@ -18,10 +18,14 @@ package im.vector.riotredesign.core.di
import android.content.Context import android.content.Context
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.core.resources.LocaleProvider import im.vector.riotredesign.core.resources.LocaleProvider
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import org.koin.dsl.module.module import org.koin.dsl.module.module
class AppModule(private val context: Context) { class AppModule(private val context: Context) {
@ -48,5 +52,22 @@ class AppModule(private val context: Context) {
RoomSelectionRepository(get()) RoomSelectionRepository(get())
} }
single {
SelectedGroupStore()
}
single {
VisibleRoomStore()
}
single {
RoomSummaryComparator()
}
factory {
Matrix.getInstance().currentSession
}
} }
} }

View file

@ -0,0 +1,48 @@
/*
* 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.core.epoxy
import android.os.Bundle
import android.os.Parcelable
import androidx.recyclerview.widget.RecyclerView
import im.vector.riotredesign.core.platform.DefaultListUpdateCallback
import im.vector.riotredesign.core.platform.Restorable
import java.util.concurrent.atomic.AtomicReference
private const val LAYOUT_MANAGER_STATE = "LAYOUT_MANAGER_STATE"
class LayoutManagerStateRestorer(private val layoutManager: RecyclerView.LayoutManager) : Restorable, DefaultListUpdateCallback {
private var layoutManagerState = AtomicReference<Parcelable?>()
override fun onSaveInstanceState(outState: Bundle) {
val layoutManagerState = layoutManager.onSaveInstanceState()
outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
val parcelable = savedInstanceState?.getParcelable<Parcelable>(LAYOUT_MANAGER_STATE)
layoutManagerState.set(parcelable)
}
override fun onInserted(position: Int, count: Int) {
layoutManagerState.getAndSet(null)?.also {
layoutManager.onRestoreInstanceState(it)
}
}
}

View file

@ -0,0 +1,27 @@
/*
* 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.core.platform
import android.os.Bundle
interface Restorable {
fun onSaveInstanceState(outState: Bundle)
fun onRestoreInstanceState(savedInstanceState: Bundle?)
}

View file

@ -16,13 +16,34 @@
package im.vector.riotredesign.core.platform package im.vector.riotredesign.core.platform
import android.os.Bundle
import androidx.annotation.MainThread
import com.airbnb.mvrx.BaseMvRxActivity import com.airbnb.mvrx.BaseMvRxActivity
import com.bumptech.glide.util.Util
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
abstract class RiotActivity : BaseMvRxActivity() { abstract class RiotActivity : BaseMvRxActivity() {
private val uiDisposables = CompositeDisposable() private val uiDisposables = CompositeDisposable()
private val restorables = ArrayList<Restorable>()
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
}
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
super.onRestoreInstanceState(savedInstanceState)
}
@MainThread
protected fun <T : Restorable> T.register(): T {
Util.assertMainThread()
restorables.add(this)
return this
}
protected fun Disposable.disposeOnDestroy(): Disposable { protected fun Disposable.disposeOnDestroy(): Disposable {
uiDisposables.add(this) uiDisposables.add(this)

View file

@ -18,8 +18,10 @@ package im.vector.riotredesign.core.platform
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.MainThread
import com.airbnb.mvrx.BaseMvRxFragment import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed { abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
@ -27,6 +29,18 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
activity as RiotActivity activity as RiotActivity
} }
private val restorables = ArrayList<Restorable>()
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
restorables.forEach { it.onSaveInstanceState(outState) }
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
restorables.forEach { it.onRestoreInstanceState(savedInstanceState) }
super.onViewStateRestored(savedInstanceState)
}
override fun onBackPressed(): Boolean { override fun onBackPressed(): Boolean {
return false return false
} }
@ -39,4 +53,11 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } } arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
} }
@MainThread
protected fun <T : Restorable> T.register(): T {
assertMainThread()
restorables.add(this)
return this
}
} }

View file

@ -19,74 +19,98 @@ package im.vector.riotredesign.features.home
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.glide.GlideRequest import im.vector.riotredesign.core.glide.GlideRequest
import kotlinx.coroutines.GlobalScope import im.vector.riotredesign.core.glide.GlideRequests
import kotlinx.coroutines.launch
/**
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
*/
object AvatarRenderer { object AvatarRenderer {
@UiThread
fun render(roomMember: RoomMember, imageView: ImageView) { fun render(roomMember: RoomMember, imageView: ImageView) {
render(roomMember.avatarUrl, roomMember.displayName, imageView) render(roomMember.avatarUrl, roomMember.displayName, imageView)
} }
@UiThread
fun render(roomSummary: RoomSummary, imageView: ImageView) { fun render(roomSummary: RoomSummary, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.displayName, imageView) render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
} }
@UiThread
fun render(avatarUrl: String?, name: String?, imageView: ImageView) { fun render(avatarUrl: String?, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, name, imageView.height, DrawableImageViewTarget(imageView))
}
@UiThread
fun render(context: Context,
glideRequest: GlideRequests,
avatarUrl: String?,
name: String?,
size: Int,
target: Target<Drawable>) {
if (name.isNullOrEmpty()) { if (name.isNullOrEmpty()) {
return return
} }
val placeholder = buildPlaceholderDrawable(imageView.context, name) val placeholder = buildPlaceholderDrawable(context, name)
buildGlideRequest(imageView.context, avatarUrl) buildGlideRequest(glideRequest, avatarUrl, size)
.placeholder(placeholder) .placeholder(placeholder)
.into(imageView) .into(target)
} }
fun load(context: Context, avatarUrl: String?, name: String?, size: Int, callback: Callback) { @WorkerThread
if (name.isNullOrEmpty()) { fun getCachedOrPlaceholder(context: Context,
return glideRequest: GlideRequests,
} avatarUrl: String?,
val request = buildGlideRequest(context, avatarUrl) text: String,
GlobalScope.launch { size: Int): Drawable {
val placeholder = buildPlaceholderDrawable(context, name) val future = buildGlideRequest(glideRequest, avatarUrl, size).onlyRetrieveFromCache(true).submit()
callback.onDrawableUpdated(placeholder) return try {
try { future.get()
val drawable = request.submit(size, size).get() } catch (exception: Exception) {
callback.onDrawableUpdated(drawable) buildPlaceholderDrawable(context, text)
} catch (exception: Exception) {
callback.onDrawableUpdated(placeholder)
}
} }
} }
private fun buildGlideRequest(context: Context, avatarUrl: String?): GlideRequest<Drawable> { // PRIVATE API *********************************************************************************
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
return GlideApp private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest<Drawable> {
.with(context) val resolvedUrl = Matrix.getInstance().currentSession
.contentUrlResolver()
.resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE)
return glideRequest
.load(resolvedUrl) .load(resolvedUrl)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.diskCacheStrategy(DiskCacheStrategy.DATA)
} }
private fun buildPlaceholderDrawable(context: Context, name: String): Drawable { private fun buildPlaceholderDrawable(context: Context, text: String): Drawable {
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
val isNameUserId = MatrixPatterns.isUserId(name) return if (text.isEmpty()) {
val firstLetterIndex = if (isNameUserId) 1 else 0 TextDrawable.builder().buildRound("", avatarColor)
val firstLetter = name[firstLetterIndex].toString().toUpperCase() } else {
return TextDrawable.builder().buildRound(firstLetter, avatarColor) val isUserId = MatrixPatterns.isUserId(text)
} val firstLetterIndex = if (isUserId) 1 else 0
val firstLetter = text[firstLetterIndex].toString().toUpperCase()
TextDrawable.builder().buildRound(firstLetter, avatarColor)
}
interface Callback {
fun onDrawableUpdated(drawable: Drawable?)
} }
} }

View file

@ -37,12 +37,12 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.standalone.StandAloneContext.loadKoinModules import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
class HomeActivity : RiotActivity(), ToolbarConfigurable { class HomeActivity : RiotActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>() private val homeNavigator by inject<HomeNavigator>()
@ -53,10 +53,10 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule(this).definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home) setContentView(R.layout.activity_home)
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
homeNavigator.activity = this
drawerLayout.addDrawerListener(drawerListener) drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val homeDrawerFragment = HomeDrawerFragment.newInstance() val homeDrawerFragment = HomeDrawerFragment.newInstance()

View file

@ -16,9 +16,9 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
import im.vector.matrix.android.api.Matrix import androidx.fragment.app.Fragment
import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.group.GroupSummaryController
import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory 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.MessageItemFactory
@ -30,89 +30,56 @@ import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFor
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryController import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.dsl.module.module import org.koin.dsl.module.module
class HomeModule(homeActivity: HomeActivity) { class HomeModule {
val definition = module(override = true) { companion object {
const val HOME_SCOPE = "HOME_SCOPE"
const val ROOM_DETAIL_SCOPE = "ROOM_DETAIL_SCOPE"
const val ROOM_LIST_SCOPE = "ROOM_LIST_SCOPE"
const val GROUP_LIST_SCOPE = "GROUP_LIST_SCOPE"
}
single { val definition = module {
Matrix.getInstance().currentSession
}
single { // Activity scope
TimelineDateFormatter(get())
}
single { scope(HOME_SCOPE) {
EventHtmlRenderer(homeActivity, get())
}
single {
MessageItemFactory(get(), get(), get(), get())
}
single {
RoomNameItemFactory(get())
}
single {
RoomTopicItemFactory(get())
}
single {
RoomMemberItemFactory(get())
}
single {
CallItemFactory(get())
}
single {
RoomHistoryVisibilityItemFactory(get())
}
single {
DefaultItemFactory()
}
single {
TimelineItemFactory(get(), get(), get(), get(), get(), get(), get())
}
single {
HomeNavigator() HomeNavigator()
} }
factory { scope(HOME_SCOPE) {
RoomSummaryController(get())
}
factory { (roomId: String) ->
TimelineEventController(roomId, get(), get(), get())
}
single {
TimelineMediaSizeProvider()
}
single {
SelectedGroupStore()
}
single {
VisibleRoomStore()
}
single {
HomePermalinkHandler(get()) HomePermalinkHandler(get())
} }
single { // Fragment scopes
RoomSummaryComparator()
scope(ROOM_DETAIL_SCOPE) { (fragment: Fragment) ->
val eventHtmlRenderer = EventHtmlRenderer(GlideApp.with(fragment), fragment.requireContext(), get())
val timelineDateFormatter = TimelineDateFormatter(get())
val timelineMediaSizeProvider = TimelineMediaSizeProvider()
val messageItemFactory = MessageItemFactory(get(), timelineMediaSizeProvider, timelineDateFormatter, eventHtmlRenderer)
val timelineItemFactory = TimelineItemFactory(messageItemFactory = messageItemFactory,
roomNameItemFactory = RoomNameItemFactory(get()),
roomTopicItemFactory = RoomTopicItemFactory(get()),
roomMemberItemFactory = RoomMemberItemFactory(get()),
roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()),
callItemFactory = CallItemFactory(get()),
defaultItemFactory = DefaultItemFactory()
)
TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider)
}
scope(ROOM_LIST_SCOPE) {
RoomSummaryController(get())
}
scope(GROUP_LIST_SCOPE) {
GroupSummaryController()
} }

View file

@ -36,9 +36,6 @@ class HomeNavigator {
eventId: String?, eventId: String?,
addToBackstack: Boolean = false) { addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack") Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoot(roomId)) {
return
}
activity?.let { activity?.let {
val args = RoomDetailArgs(roomId, eventId) val args = RoomDetailArgs(roomId, eventId)
val roomDetailFragment = RoomDetailFragment.newInstance(args) val roomDetailFragment = RoomDetailFragment.newInstance(args)

View file

@ -27,7 +27,11 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import kotlinx.android.synthetic.main.fragment_group_list.* import kotlinx.android.synthetic.main.fragment_group_list.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
class GroupListFragment : RiotFragment(), GroupSummaryController.Callback { class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
@ -38,8 +42,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
} }
private val viewModel: GroupListViewModel by fragmentViewModel() private val viewModel: GroupListViewModel by fragmentViewModel()
private val groupController by inject<GroupSummaryController>()
private lateinit var groupController: GroupSummaryController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_group_list, container, false) return inflater.inflate(R.layout.fragment_group_list, container, false)
@ -47,7 +50,8 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
groupController = GroupSummaryController(this) bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
groupController.callback = this
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(groupController) epoxyRecyclerView.setController(groupController)
viewModel.subscribe { renderState(it) } viewModel.subscribe { renderState(it) }
@ -56,7 +60,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
private fun renderState(state: GroupListViewState) { private fun renderState(state: GroupListViewState) {
when (state.asyncGroups) { when (state.asyncGroups) {
is Incomplete -> renderLoading() is Incomplete -> renderLoading()
is Success -> renderSuccess(state) is Success -> renderSuccess(state)
} }
} }

View file

@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.group
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
@ -34,7 +33,7 @@ class GroupListViewModel(initialState: GroupListViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? { override fun create(viewModelContext: ViewModelContext, state: GroupListViewState): GroupListViewModel? {
val currentSession = Matrix.getInstance().currentSession val currentSession = viewModelContext.activity.get<Session>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
return GroupListViewModel(state, selectedGroupHolder, currentSession) return GroupListViewModel(state, selectedGroupHolder, currentSession)
} }

View file

@ -19,8 +19,9 @@ package im.vector.riotredesign.features.home.group
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
class GroupSummaryController(private val callback: Callback? = null class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
) : TypedEpoxyController<GroupListViewState>() {
var callback: Callback? = null
override fun buildModels(viewState: GroupListViewState) { override fun buildModels(viewState: GroupListViewState) {
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)

View file

@ -25,18 +25,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@Parcelize @Parcelize
@ -45,6 +48,7 @@ data class RoomDetailArgs(
val eventId: String? = null val eventId: String? = null
) : Parcelable ) : Parcelable
class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
companion object { companion object {
@ -57,10 +61,9 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }
private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel()
private val roomDetailArgs: RoomDetailArgs by args() private val timelineEventController: TimelineEventController by inject { parametersOf(this) }
private val homePermalinkHandler: HomePermalinkHandler by inject()
private val timelineEventController by inject<TimelineEventController> { parametersOf(roomDetailArgs.roomId) }
private val homePermalinkHandler by inject<HomePermalinkHandler>()
private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,6 +72,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
setupRecyclerView() setupRecyclerView()
setupToolbar() setupToolbar()
setupSendButton() setupSendButton()
@ -80,6 +84,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
roomDetailViewModel.process(RoomDetailActions.IsDisplayed) roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
} }
// PRIVATE METHODS *****************************************************************************
private fun setupToolbar() { private fun setupToolbar() {
val parentActivity = riotActivity val parentActivity = riotActivity
if (parentActivity is ToolbarConfigurable) { if (parentActivity is ToolbarConfigurable) {
@ -91,10 +97,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val epoxyVisibilityTracker = EpoxyVisibilityTracker() val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView) epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) } timelineEventController.addModelBuildListener {
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
}
recyclerView.setController(timelineEventController) recyclerView.setController(timelineEventController)
timelineEventController.callback = this timelineEventController.callback = this
} }

View file

@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.room.detail
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -46,7 +45,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? { override fun create(viewModelContext: ViewModelContext, state: RoomDetailViewState): RoomDetailViewModel? {
val currentSession = Matrix.getInstance().currentSession val currentSession = viewModelContext.activity.get<Session>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>() val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
return RoomDetailViewModel(state, currentSession, visibleRoomHolder) return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
} }

View file

@ -16,13 +16,18 @@
package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline
import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.util.Linkify import android.text.util.Linkify
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan 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.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.* 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.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.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
@ -146,7 +151,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
.informationData(informationData) .informationData(informationData)
} }
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
val spannable = SpannableStringBuilder(body) val spannable = SpannableStringBuilder(body)
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
override fun onUrlClicked(url: String) { override fun onUrlClicked(url: String) {

View file

@ -16,30 +16,54 @@
package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline
import android.text.Spannable
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat
import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.features.html.PillImageSpan
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@EpoxyModelClass(layout = R.layout.item_timeline_event_text_message) @EpoxyModelClass(layout = R.layout.item_timeline_event_text_message)
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
@EpoxyAttribute var message: CharSequence? = null @EpoxyAttribute var message: Spannable? = null
@EpoxyAttribute override lateinit var informationData: MessageInformationData @EpoxyAttribute override lateinit var informationData: MessageInformationData
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.messageView.text = message
MatrixLinkify.addLinkMovementMethod(holder.messageView) MatrixLinkify.addLinkMovementMethod(holder.messageView)
val textFuture = PrecomputedTextCompat.getTextFuture(message ?: "",
TextViewCompat.getTextMetricsParams(holder.messageView),
null)
holder.messageView.setTextFuture(textFuture)
findPillsAndProcess { it.bind(holder.messageView) }
}
private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) {
GlobalScope.launch(Dispatchers.Main) {
val pillImageSpans: Array<PillImageSpan>? = withContext(Dispatchers.IO) {
message?.let { spannable ->
spannable.getSpans(0, spannable.length, PillImageSpan::class.java)
}
}
pillImageSpans?.forEach { processBlock(it) }
}
} }
class Holder : AbsMessageItem.Holder() { class Holder : AbsMessageItem.Holder() {
override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) override val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
override val memberNameView by bind<TextView>(R.id.messageMemberNameView) override val memberNameView by bind<TextView>(R.id.messageMemberNameView)
override val timeView by bind<TextView>(R.id.messageTimeView) override val timeView by bind<TextView>(R.id.messageTimeView)
val messageView by bind<TextView>(R.id.messageTextView) val messageView by bind<AppCompatTextView>(R.id.messageTextView)
} }

View file

@ -29,8 +29,7 @@ import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController import im.vector.riotredesign.features.home.room.detail.timeline.paging.PagedListEpoxyController
class TimelineEventController(private val roomId: String, class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
private val dateFormatter: TimelineDateFormatter,
private val timelineItemFactory: TimelineItemFactory, private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider private val timelineMediaSizeProvider: TimelineMediaSizeProvider
) : PagedListEpoxyController<TimelineEvent>( ) : PagedListEpoxyController<TimelineEvent>(
@ -82,7 +81,7 @@ class TimelineEventController(private val roomId: String,
} }
if (addDaySeparator) { if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date) val formattedDay = dateFormatter.formatMessageDay(date)
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(roomId + formattedDay) val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
epoxyModels.add(daySeparatorItem) epoxyModels.add(daySeparatorItem)
} }
return epoxyModels return epoxyModels
@ -90,13 +89,13 @@ class TimelineEventController(private val roomId: String,
override fun addModels(models: List<EpoxyModel<*>>) { override fun addModels(models: List<EpoxyModel<*>>) {
LoadingItemModel_() LoadingItemModel_()
.id(roomId + "forward_loading_item") .id("forward_loading_item")
.addIf(isLoadingForward, this) .addIf(isLoadingForward, this)
super.add(models) super.add(models)
LoadingItemModel_() LoadingItemModel_()
.id(roomId + "backward_loading_item") .id("backward_loading_item")
.addIf(!hasReachedEnd, this) .addIf(!hasReachedEnd, this)
} }

View file

@ -22,8 +22,8 @@ sealed class RoomListActions {
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions() data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
object RoomDisplayed : RoomListActions()
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions() data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
} }

View file

@ -22,19 +22,25 @@ import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.setupAsSearch import im.vector.riotredesign.core.extensions.setupAsSearch
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.StateView import im.vector.riotredesign.core.platform.StateView
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomeNavigator import im.vector.riotredesign.features.home.HomeNavigator
import kotlinx.android.synthetic.main.fragment_room_list.* import kotlinx.android.synthetic.main.fragment_room_list.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
class RoomListFragment : RiotFragment(), RoomSummaryController.Callback { class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
@ -44,9 +50,9 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
} }
} }
private val homeNavigator by inject<HomeNavigator>()
private val roomController by inject<RoomSummaryController>() private val roomController by inject<RoomSummaryController>()
private val homeViewModel: RoomListViewModel by activityViewModel() private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false) return inflater.inflate(R.layout.fragment_room_list, container, false)
@ -54,11 +60,36 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
bindScope(getOrCreateScope(HomeModule.ROOM_LIST_SCOPE))
setupRecyclerView()
setupFilterView()
roomListViewModel.subscribe { renderState(it) }
roomListViewModel.openRoomLiveData.observeEvent(this) {
homeNavigator.openRoomDetail(it, null)
}
}
private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
epoxyRecyclerView.layoutManager = layoutManager
roomController.callback = this roomController.callback = this
roomController.addModelBuildListener { it.dispatchTo(stateRestorer) }
stateView.contentView = epoxyRecyclerView stateView.contentView = epoxyRecyclerView
epoxyRecyclerView.setController(roomController) epoxyRecyclerView.setController(roomController)
setupFilterView() }
homeViewModel.subscribe { renderState(it) }
private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
roomListViewModel.accept(RoomListActions.FilterRooms(s))
}
})
} }
private fun renderState(state: RoomListViewState) { private fun renderState(state: RoomListViewState) {
@ -90,24 +121,13 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
stateView.state = StateView.State.Error(message) stateView.state = StateView.State.Error(message)
} }
private fun setupFilterView() {
filterRoomView.setupAsSearch()
filterRoomView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
homeViewModel.accept(RoomListActions.FilterRooms(s))
}
})
}
// RoomSummaryController.Callback ************************************************************** // RoomSummaryController.Callback **************************************************************
override fun onRoomSelected(room: RoomSummary) { override fun onRoomSelected(room: RoomSummary) {
homeViewModel.accept(RoomListActions.SelectRoom(room)) roomListViewModel.accept(RoomListActions.SelectRoom(room))
homeNavigator.openRoomDetail(room.roomId, null)
} }
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
}
} }

View file

@ -16,22 +16,23 @@
package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option import arrow.core.Option
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.tag.RoomTag import im.vector.matrix.android.api.session.room.model.tag.RoomTag
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.core.utils.LiveEvent
import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.VisibleRoomStore
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.Function3 import io.reactivex.functions.Function3
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -49,7 +50,7 @@ class RoomListViewModel(initialState: RoomListViewState,
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? { override fun create(viewModelContext: ViewModelContext, state: RoomListViewState): RoomListViewModel? {
val currentSession = Matrix.getInstance().currentSession val currentSession = viewModelContext.activity.get<Session>()
val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>() val roomSelectionRepository = viewModelContext.activity.get<RoomSelectionRepository>()
val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>() val selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>() val visibleRoomHolder = viewModelContext.activity.get<VisibleRoomStore>()
@ -61,6 +62,10 @@ class RoomListViewModel(initialState: RoomListViewState,
private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty()) private val roomListFilter = BehaviorRelay.createDefault<Option<RoomListFilterName>>(Option.empty())
private val _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
val openRoomLiveData: LiveData<LiveEvent<String>>
get() = _openRoomLiveData
init { init {
observeRoomSummaries() observeRoomSummaries()
observeVisibleRoom() observeVisibleRoom()
@ -68,16 +73,18 @@ class RoomListViewModel(initialState: RoomListViewState,
fun accept(action: RoomListActions) { fun accept(action: RoomListActions) {
when (action) { when (action) {
is RoomListActions.SelectRoom -> handleSelectRoom(action) is RoomListActions.SelectRoom -> handleSelectRoom(action)
is RoomListActions.FilterRooms -> handleFilterRooms(action) is RoomListActions.FilterRooms -> handleFilterRooms(action)
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
} }
} }
// PRIVATE METHODS ***************************************************************************** // PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state -> private fun handleSelectRoom(action: RoomListActions.SelectRoom) = withState { state ->
if (state.selectedRoomId != action.roomSummary.roomId) { if (state.visibleRoomId != action.roomSummary.roomId) {
roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId) roomSelectionRepository.saveLastSelectedRoom(action.roomSummary.roomId)
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
} }
} }
@ -86,10 +93,14 @@ class RoomListViewModel(initialState: RoomListViewState,
roomListFilter.accept(optionalFilter) roomListFilter.accept(optionalFilter)
} }
private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
this.toggle(action.category)
}
private fun observeVisibleRoom() { private fun observeVisibleRoom() {
visibleRoomHolder.observe() visibleRoomHolder.observe()
.doOnNext { .doOnNext {
setState { copy(selectedRoomId = it) } setState { copy(visibleRoomId = it) }
} }
.subscribe() .subscribe()
.disposeOnClear() .disposeOnClear()
@ -159,13 +170,13 @@ class RoomListViewModel(initialState: RoomListViewState,
} }
} }
return RoomSummaries( return RoomSummaries().apply {
favourites = favourites.sortedWith(roomSummaryComparator), put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
directRooms = directChats.sortedWith(roomSummaryComparator), put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
groupRooms = groupRooms.sortedWith(roomSummaryComparator), put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator), put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
serverNotices = serverNotices.sortedWith(roomSummaryComparator) put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
) }
} }

View file

@ -16,24 +16,54 @@
package im.vector.riotredesign.features.home.room.list package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
data class RoomListViewState( data class RoomListViewState(
val asyncRooms: Async<RoomSummaries> = Uninitialized, val asyncRooms: Async<RoomSummaries> = Uninitialized,
val selectedRoomId: String? = null val visibleRoomId: String? = null,
) : MvRxState val isFavouriteRoomsExpanded: Boolean = true,
val isDirectRoomsExpanded: Boolean = false,
val isGroupRoomsExpanded: Boolean = false,
val isLowPriorityRoomsExpanded: Boolean = false,
val isServerNoticeRoomsExpanded: Boolean = false
) : MvRxState {
data class RoomSummaries( fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
val favourites: List<RoomSummary>, return when (roomCategory) {
val directRooms: List<RoomSummary>, RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
val groupRooms: List<RoomSummary>, RoomCategory.DIRECT -> isDirectRoomsExpanded
val lowPriorities: List<RoomSummary>, RoomCategory.GROUP -> isGroupRoomsExpanded
val serverNotices: List<RoomSummary> RoomCategory.LOW_PRIORITY -> isLowPriorityRoomsExpanded
) RoomCategory.SERVER_NOTICE -> isServerNoticeRoomsExpanded
}
}
fun toggle(roomCategory: RoomCategory): RoomListViewState {
return when (roomCategory) {
RoomCategory.FAVOURITE -> copy(isFavouriteRoomsExpanded = !isFavouriteRoomsExpanded)
RoomCategory.DIRECT -> copy(isDirectRoomsExpanded = !isDirectRoomsExpanded)
RoomCategory.GROUP -> copy(isGroupRoomsExpanded = !isGroupRoomsExpanded)
RoomCategory.LOW_PRIORITY -> copy(isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded)
RoomCategory.SERVER_NOTICE -> copy(isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded)
}
}
}
typealias RoomSummaries = LinkedHashMap<RoomCategory, List<RoomSummary>>
enum class RoomCategory(@StringRes val titleRes: Int) {
FAVOURITE(R.string.room_list_favourites),
DIRECT(R.string.room_list_direct),
GROUP(R.string.room_list_group),
LOW_PRIORITY(R.string.room_list_low_priority),
SERVER_NOTICE(R.string.room_list_system_alert)
}
fun RoomSummaries?.isNullOrEmpty(): Boolean { fun RoomSummaries?.isNullOrEmpty(): Boolean {
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty()) return this == null || isEmpty()
} }

View file

@ -19,65 +19,34 @@ package im.vector.riotredesign.features.home.room.list
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R
import im.vector.riotredesign.core.resources.StringProvider import im.vector.riotredesign.core.resources.StringProvider
class RoomSummaryController(private val stringProvider: StringProvider class RoomSummaryController(private val stringProvider: StringProvider
) : TypedEpoxyController<RoomListViewState>() { ) : TypedEpoxyController<RoomListViewState>() {
private var isFavoriteRoomsExpanded = true
private var isDirectRoomsExpanded = false
private var isGroupRoomsExpanded = false
private var isLowPriorityRoomsExpanded = false
private var isServerNoticeRoomsExpanded = false
var callback: Callback? = null var callback: Callback? = null
override fun buildModels(viewState: RoomListViewState) { override fun buildModels(viewState: RoomListViewState) {
val roomSummaries = viewState.asyncRooms() val roomSummaries = viewState.asyncRooms()
val favourites = roomSummaries?.favourites ?: emptyList() roomSummaries?.forEach { (category, summaries) ->
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) { if (summaries.isEmpty()) {
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded return@forEach
} else {
val isExpanded = viewState.isCategoryExpanded(category)
buildRoomCategory(viewState, summaries, category.titleRes, viewState.isCategoryExpanded(category)) {
callback?.onToggleRoomCategory(category)
}
if (isExpanded) {
buildRoomModels(summaries, viewState.visibleRoomId)
}
}
} }
if (isFavoriteRoomsExpanded) {
buildRoomModels(favourites, viewState.selectedRoomId)
}
val directRooms = roomSummaries?.directRooms ?: emptyList()
buildRoomCategory(viewState, directRooms, R.string.room_list_direct, isDirectRoomsExpanded) {
isDirectRoomsExpanded = !isDirectRoomsExpanded
}
if (isDirectRoomsExpanded) {
buildRoomModels(directRooms, viewState.selectedRoomId)
}
val groupRooms = roomSummaries?.groupRooms ?: emptyList()
buildRoomCategory(viewState, groupRooms, R.string.room_list_group, isGroupRoomsExpanded) {
isGroupRoomsExpanded = !isGroupRoomsExpanded
}
if (isGroupRoomsExpanded) {
buildRoomModels(groupRooms, viewState.selectedRoomId)
}
val lowPriorities = roomSummaries?.lowPriorities ?: emptyList()
buildRoomCategory(viewState, lowPriorities, R.string.room_list_low_priority, isLowPriorityRoomsExpanded) {
isLowPriorityRoomsExpanded = !isLowPriorityRoomsExpanded
}
if (isLowPriorityRoomsExpanded) {
buildRoomModels(lowPriorities, viewState.selectedRoomId)
}
val serverNotices = roomSummaries?.serverNotices ?: emptyList()
buildRoomCategory(viewState, serverNotices, R.string.room_list_system_alert, isServerNoticeRoomsExpanded) {
isServerNoticeRoomsExpanded = !isServerNoticeRoomsExpanded
}
if (isServerNoticeRoomsExpanded) {
buildRoomModels(serverNotices, viewState.selectedRoomId)
}
} }
private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) { private fun buildRoomCategory(viewState: RoomListViewState, summaries: List<RoomSummary>, @StringRes titleRes: Int, isExpanded: Boolean, mutateExpandedState: () -> Unit) {
if (summaries.isEmpty()) {
return
}
//TODO should add some business logic later //TODO should add some business logic later
val unreadCount = if (summaries.isEmpty()) { val unreadCount = if (summaries.isEmpty()) {
0 0
@ -117,6 +86,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
} }
interface Callback { interface Callback {
fun onToggleRoomCategory(roomCategory: RoomCategory)
fun onRoomSelected(room: RoomSummary) fun onRoomSelected(room: RoomSummary)
} }

View file

@ -19,10 +19,10 @@
package im.vector.riotredesign.features.html package im.vector.riotredesign.features.html
import android.content.Context import android.content.Context
import android.text.style.ImageSpan
import im.vector.matrix.android.api.permalinks.PermalinkData import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.core.glide.GlideRequests
import org.commonmark.node.BlockQuote import org.commonmark.node.BlockQuote
import org.commonmark.node.HtmlBlock import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline import org.commonmark.node.HtmlInline
@ -49,11 +49,12 @@ import ru.noties.markwon.html.tag.SuperScriptHandler
import ru.noties.markwon.html.tag.UnderlineHandler import ru.noties.markwon.html.tag.UnderlineHandler
import java.util.Arrays.asList import java.util.Arrays.asList
class EventHtmlRenderer(private val context: Context, class EventHtmlRenderer(glideRequests: GlideRequests,
private val session: Session) { context: Context,
session: Session) {
private val markwon = Markwon.builder(context) private val markwon = Markwon.builder(context)
.usePlugin(MatrixPlugin.create(context, session)) .usePlugin(MatrixPlugin.create(glideRequests, context, session))
.build() .build()
fun render(text: String): CharSequence { fun render(text: String): CharSequence {
@ -62,7 +63,8 @@ class EventHtmlRenderer(private val context: Context,
} }
private class MatrixPlugin private constructor(private val context: Context, private class MatrixPlugin private constructor(private val glideRequests: GlideRequests,
private val context: Context,
private val session: Session) : AbstractMarkwonPlugin() { private val session: Session) : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
@ -76,7 +78,7 @@ private class MatrixPlugin private constructor(private val context: Context,
ImageHandler.create()) ImageHandler.create())
.addHandler( .addHandler(
"a", "a",
MxLinkHandler(context, session)) MxLinkHandler(glideRequests, context, session))
.addHandler( .addHandler(
"blockquote", "blockquote",
BlockquoteHandler()) BlockquoteHandler())
@ -128,13 +130,15 @@ private class MatrixPlugin private constructor(private val context: Context,
companion object { companion object {
fun create(context: Context, session: Session): MatrixPlugin { fun create(glideRequests: GlideRequests, context: Context, session: Session): MatrixPlugin {
return MatrixPlugin(context, session) return MatrixPlugin(glideRequests, context, session)
} }
} }
} }
private class MxLinkHandler(private val context: Context, private val session: Session) : TagHandler() { private class MxLinkHandler(private val glideRequests: GlideRequests,
private val context: Context,
private val session: Session) : TagHandler() {
private val linkHandler = LinkHandler() private val linkHandler = LinkHandler()
@ -144,9 +148,8 @@ private class MxLinkHandler(private val context: Context, private val session: S
val permalinkData = PermalinkParser.parse(link) val permalinkData = PermalinkParser.parse(link)
when (permalinkData) { when (permalinkData) {
is PermalinkData.UserLink -> { is PermalinkData.UserLink -> {
val user = session.getUser(permalinkData.userId) ?: return val user = session.getUser(permalinkData.userId)
val drawable = PillDrawableFactory.create(context, permalinkData.userId, user) val span = PillImageSpan(glideRequests, context, permalinkData.userId, user)
val span = ImageSpan(drawable)
SpannableBuilder.setSpans( SpannableBuilder.setSpans(
visitor.builder(), visitor.builder(),
span, span,

View file

@ -1,59 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.html
import android.content.Context
import android.graphics.drawable.Drawable
import com.google.android.material.chip.ChipDrawable
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.AvatarRenderer
import java.lang.ref.WeakReference
object PillDrawableFactory {
fun create(context: Context, userId: String, user: User?): Drawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val chipDrawable = ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
setText(user?.displayName ?: userId)
textEndPadding = textPadding
textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
val avatarRendererCallback = AvatarRendererChipCallback(chipDrawable)
AvatarRenderer.load(context, user?.avatarUrl, user?.displayName, 80, avatarRendererCallback)
return chipDrawable
}
private class AvatarRendererChipCallback(chipDrawable: ChipDrawable) : AvatarRenderer.Callback {
private val weakChipDrawable = WeakReference<ChipDrawable>(chipDrawable)
override fun onDrawableUpdated(drawable: Drawable?) {
weakChipDrawable.get()?.apply {
chipIcon = drawable
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}
}
}

View file

@ -0,0 +1,138 @@
/*
* 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.html
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.style.ReplacementSpan
import android.widget.TextView
import androidx.annotation.UiThread
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.google.android.material.chip.ChipDrawable
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.core.glide.GlideRequests
import im.vector.riotredesign.features.home.AvatarRenderer
import java.lang.ref.WeakReference
/**
* This span is able to replace a text by a [ChipDrawable]
* It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached.
*/
private const val PILL_AVATAR_SIZE = 80
class PillImageSpan(private val glideRequests: GlideRequests,
private val context: Context,
private val userId: String,
private val user: User?) : ReplacementSpan() {
private val displayName by lazy {
if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!!
}
private val pillDrawable = createChipDrawable()
private val target = PillImageSpanTarget(this)
private var tv: WeakReference<TextView>? = null
@UiThread
fun bind(textView: TextView) {
tv = WeakReference(textView)
AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE, target)
}
// ReplacementSpan *****************************************************************************
override fun getSize(paint: Paint, text: CharSequence,
start: Int,
end: Int,
fm: Paint.FontMetricsInt?): Int {
val rect = pillDrawable.bounds
if (fm != null) {
fm.ascent = -rect.bottom
fm.descent = 0
fm.top = fm.ascent
fm.bottom = 0
}
return rect.right
}
override fun draw(canvas: Canvas, text: CharSequence,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint) {
canvas.save()
val transY = bottom - pillDrawable.bounds.bottom
canvas.translate(x, transY.toFloat())
pillDrawable.draw(canvas)
canvas.restore()
}
internal fun updateAvatarDrawable(drawable: Drawable?) {
pillDrawable.apply {
chipIcon = drawable
}
tv?.get()?.apply {
invalidate()
}
}
// Private methods *****************************************************************************
private fun createChipDrawable(): ChipDrawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
return ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
setText(displayName)
textEndPadding = textPadding
textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
chipIcon = AvatarRenderer.getCachedOrPlaceholder(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE)
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}
}
/**
* Glide target to handle avatar retrieval into [PillImageSpan].
*/
private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget<Drawable>() {
private val pillImageSpan = WeakReference(pillImageSpan)
override fun onResourceReady(drawable: Drawable, transition: Transition<in Drawable>?) {
updateWith(drawable)
}
override fun onLoadCleared(placeholder: Drawable?) {
updateWith(placeholder)
}
private fun updateWith(drawable: Drawable?) {
pillImageSpan.get()?.apply {
updateAvatarDrawable(drawable)
}
}
}

View file

@ -15,7 +15,6 @@
android:layout_marginStart="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
@ -24,14 +23,15 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textColor="@color/slate_grey" android:textColor="@color/slate_grey"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="italic" android:textStyle="italic"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/itemNoticeAvatarView" app:layout_constraintStart_toEndOf="@+id/itemNoticeAvatarView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@+id/itemNoticeAvatarView"
tools:text="Mon item" /> tools:text="Mon item" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>