Merge branch 'feature/home_state_issues' into develop
This commit is contained in:
commit
a146fc43e4
|
@ -3,9 +3,14 @@
|
|||
<words>
|
||||
<w>connectable</w>
|
||||
<w>coroutine</w>
|
||||
<w>linkify</w>
|
||||
<w>markon</w>
|
||||
<w>markwon</w>
|
||||
<w>merlins</w>
|
||||
<w>moshi</w>
|
||||
<w>persistor</w>
|
||||
<w>restorable</w>
|
||||
<w>restorables</w>
|
||||
<w>synchronizer</w>
|
||||
<w>untimelined</w>
|
||||
</words>
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.facebook.stetho.Stetho
|
|||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.riotredesign.core.di.AppModule
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import org.koin.log.EmptyLogger
|
||||
import org.koin.standalone.StandAloneContext.startKoin
|
||||
import timber.log.Timber
|
||||
|
@ -37,7 +38,9 @@ class Riot : Application() {
|
|||
Stetho.initializeWithDefaults(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) {
|
||||
|
|
|
@ -18,10 +18,14 @@ package im.vector.riotredesign.core.di
|
|||
|
||||
import android.content.Context
|
||||
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.LocaleProvider
|
||||
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.RoomSummaryComparator
|
||||
import org.koin.dsl.module.module
|
||||
|
||||
class AppModule(private val context: Context) {
|
||||
|
@ -48,5 +52,22 @@ class AppModule(private val context: Context) {
|
|||
RoomSelectionRepository(get())
|
||||
}
|
||||
|
||||
single {
|
||||
SelectedGroupStore()
|
||||
}
|
||||
|
||||
single {
|
||||
VisibleRoomStore()
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSummaryComparator()
|
||||
}
|
||||
|
||||
factory {
|
||||
Matrix.getInstance().currentSession
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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?)
|
||||
|
||||
}
|
|
@ -16,13 +16,34 @@
|
|||
|
||||
package im.vector.riotredesign.core.platform
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.MainThread
|
||||
import com.airbnb.mvrx.BaseMvRxActivity
|
||||
import com.bumptech.glide.util.Util
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class RiotActivity : BaseMvRxActivity() {
|
||||
|
||||
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 {
|
||||
uiDisposables.add(this)
|
||||
|
|
|
@ -18,8 +18,10 @@ package im.vector.riotredesign.core.platform
|
|||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.MainThread
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.bumptech.glide.util.Util.assertMainThread
|
||||
|
||||
abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
|
||||
|
||||
|
@ -27,6 +29,18 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
|
|||
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 {
|
||||
return false
|
||||
}
|
||||
|
@ -39,4 +53,11 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
|
|||
arguments = args?.let { Bundle().apply { putParcelable(MvRx.KEY_ARG, it) } }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
protected fun <T : Restorable> T.register(): T {
|
||||
assertMainThread()
|
||||
restorables.add(this)
|
||||
return this
|
||||
}
|
||||
|
||||
}
|
|
@ -19,74 +19,98 @@ package im.vector.riotredesign.features.home
|
|||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
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.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
|
||||
import im.vector.riotredesign.core.glide.GlideRequest
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import im.vector.riotredesign.core.glide.GlideRequests
|
||||
|
||||
/**
|
||||
* This helper centralise ways to retrieve avatar into ImageView or even generic Target<Drawable>
|
||||
*/
|
||||
object AvatarRenderer {
|
||||
|
||||
@UiThread
|
||||
fun render(roomMember: RoomMember, imageView: ImageView) {
|
||||
render(roomMember.avatarUrl, roomMember.displayName, imageView)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun render(roomSummary: RoomSummary, imageView: ImageView) {
|
||||
render(roomSummary.avatarUrl, roomSummary.displayName, imageView)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
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()) {
|
||||
return
|
||||
}
|
||||
val placeholder = buildPlaceholderDrawable(imageView.context, name)
|
||||
buildGlideRequest(imageView.context, avatarUrl)
|
||||
val placeholder = buildPlaceholderDrawable(context, name)
|
||||
buildGlideRequest(glideRequest, avatarUrl, size)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
.into(target)
|
||||
}
|
||||
|
||||
fun load(context: Context, avatarUrl: String?, name: String?, size: Int, callback: Callback) {
|
||||
if (name.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val request = buildGlideRequest(context, avatarUrl)
|
||||
GlobalScope.launch {
|
||||
val placeholder = buildPlaceholderDrawable(context, name)
|
||||
callback.onDrawableUpdated(placeholder)
|
||||
try {
|
||||
val drawable = request.submit(size, size).get()
|
||||
callback.onDrawableUpdated(drawable)
|
||||
} catch (exception: Exception) {
|
||||
callback.onDrawableUpdated(placeholder)
|
||||
}
|
||||
@WorkerThread
|
||||
fun getCachedOrPlaceholder(context: Context,
|
||||
glideRequest: GlideRequests,
|
||||
avatarUrl: String?,
|
||||
text: String,
|
||||
size: Int): Drawable {
|
||||
val future = buildGlideRequest(glideRequest, avatarUrl, size).onlyRetrieveFromCache(true).submit()
|
||||
return try {
|
||||
future.get()
|
||||
} catch (exception: Exception) {
|
||||
buildPlaceholderDrawable(context, text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildGlideRequest(context: Context, avatarUrl: String?): GlideRequest<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
|
||||
return GlideApp
|
||||
.with(context)
|
||||
// PRIVATE API *********************************************************************************
|
||||
|
||||
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest<Drawable> {
|
||||
val resolvedUrl = Matrix.getInstance().currentSession
|
||||
.contentUrlResolver()
|
||||
.resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE)
|
||||
|
||||
return glideRequest
|
||||
.load(resolvedUrl)
|
||||
.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 isNameUserId = MatrixPatterns.isUserId(name)
|
||||
val firstLetterIndex = if (isNameUserId) 1 else 0
|
||||
val firstLetter = name[firstLetterIndex].toString().toUpperCase()
|
||||
return TextDrawable.builder().buildRound(firstLetter, avatarColor)
|
||||
}
|
||||
return if (text.isEmpty()) {
|
||||
TextDrawable.builder().buildRound("", avatarColor)
|
||||
} else {
|
||||
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?)
|
||||
}
|
||||
|
||||
}
|
|
@ -37,12 +37,12 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
|
|||
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
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 {
|
||||
|
||||
|
||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||
private val homeNavigator by inject<HomeNavigator>()
|
||||
|
||||
|
@ -53,10 +53,10 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
loadKoinModules(listOf(HomeModule(this).definition))
|
||||
homeNavigator.activity = this
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_home)
|
||||
bindScope(getOrCreateScope(HomeModule.HOME_SCOPE))
|
||||
homeNavigator.activity = this
|
||||
drawerLayout.addDrawerListener(drawerListener)
|
||||
if (savedInstanceState == null) {
|
||||
val homeDrawerFragment = HomeDrawerFragment.newInstance()
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
|
||||
package im.vector.riotredesign.features.home
|
||||
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.riotredesign.features.home.group.SelectedGroupStore
|
||||
import im.vector.riotredesign.features.home.room.VisibleRoomStore
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotredesign.core.glide.GlideApp
|
||||
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.DefaultItemFactory
|
||||
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.TimelineItemFactory
|
||||
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.html.EventHtmlRenderer
|
||||
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 {
|
||||
Matrix.getInstance().currentSession
|
||||
}
|
||||
val definition = module {
|
||||
|
||||
single {
|
||||
TimelineDateFormatter(get())
|
||||
}
|
||||
// Activity scope
|
||||
|
||||
single {
|
||||
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 {
|
||||
scope(HOME_SCOPE) {
|
||||
HomeNavigator()
|
||||
}
|
||||
|
||||
factory {
|
||||
RoomSummaryController(get())
|
||||
}
|
||||
|
||||
factory { (roomId: String) ->
|
||||
TimelineEventController(roomId, get(), get(), get())
|
||||
}
|
||||
|
||||
single {
|
||||
TimelineMediaSizeProvider()
|
||||
}
|
||||
|
||||
single {
|
||||
SelectedGroupStore()
|
||||
}
|
||||
|
||||
single {
|
||||
VisibleRoomStore()
|
||||
}
|
||||
|
||||
single {
|
||||
scope(HOME_SCOPE) {
|
||||
HomePermalinkHandler(get())
|
||||
}
|
||||
|
||||
single {
|
||||
RoomSummaryComparator()
|
||||
// Fragment scopes
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -36,9 +36,6 @@ class HomeNavigator {
|
|||
eventId: String?,
|
||||
addToBackstack: Boolean = false) {
|
||||
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
|
||||
if (!addToBackstack && isRoot(roomId)) {
|
||||
return
|
||||
}
|
||||
activity?.let {
|
||||
val args = RoomDetailArgs(roomId, eventId)
|
||||
val roomDetailFragment = RoomDetailFragment.newInstance(args)
|
||||
|
|
|
@ -27,7 +27,11 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
|
|||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
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 {
|
||||
|
||||
|
@ -38,8 +42,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
|
|||
}
|
||||
|
||||
private val viewModel: GroupListViewModel by fragmentViewModel()
|
||||
|
||||
private lateinit var groupController: GroupSummaryController
|
||||
private val groupController by inject<GroupSummaryController>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_group_list, container, false)
|
||||
|
@ -47,7 +50,8 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
|
|||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
groupController = GroupSummaryController(this)
|
||||
bindScope(getOrCreateScope(HomeModule.GROUP_LIST_SCOPE))
|
||||
groupController.callback = this
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
epoxyRecyclerView.setController(groupController)
|
||||
viewModel.subscribe { renderState(it) }
|
||||
|
@ -56,7 +60,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
|
|||
private fun renderState(state: GroupListViewState) {
|
||||
when (state.asyncGroups) {
|
||||
is Incomplete -> renderLoading()
|
||||
is Success -> renderSuccess(state)
|
||||
is Success -> renderSuccess(state)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.group
|
|||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import im.vector.matrix.android.api.Matrix
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotredesign.core.platform.RiotViewModel
|
||||
|
@ -34,7 +33,7 @@ class GroupListViewModel(initialState: GroupListViewState,
|
|||
|
||||
@JvmStatic
|
||||
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>()
|
||||
return GroupListViewModel(state, selectedGroupHolder, currentSession)
|
||||
}
|
||||
|
|
|
@ -19,8 +19,9 @@ package im.vector.riotredesign.features.home.group
|
|||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
|
||||
class GroupSummaryController(private val callback: Callback? = null
|
||||
) : TypedEpoxyController<GroupListViewState>() {
|
||||
class GroupSummaryController : TypedEpoxyController<GroupListViewState>() {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun buildModels(viewState: GroupListViewState) {
|
||||
buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup)
|
||||
|
|
|
@ -25,18 +25,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.epoxy.EpoxyVisibilityTracker
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.args
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
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.ToolbarConfigurable
|
||||
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.room.detail.timeline.TimelineEventController
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
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
|
||||
|
||||
@Parcelize
|
||||
|
@ -45,6 +48,7 @@ data class RoomDetailArgs(
|
|||
val eventId: String? = null
|
||||
) : Parcelable
|
||||
|
||||
|
||||
class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
||||
|
||||
companion object {
|
||||
|
@ -57,10 +61,9 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||
}
|
||||
|
||||
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
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -69,6 +72,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
bindScope(getOrCreateScope(HomeModule.ROOM_DETAIL_SCOPE))
|
||||
setupRecyclerView()
|
||||
setupToolbar()
|
||||
setupSendButton()
|
||||
|
@ -80,6 +84,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
private fun setupToolbar() {
|
||||
val parentActivity = riotActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
|
@ -91,10 +97,14 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
|
|||
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
|
||||
epoxyVisibilityTracker.attach(recyclerView)
|
||||
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
|
||||
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
|
||||
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.setHasFixedSize(true)
|
||||
timelineEventController.addModelBuildListener { it.dispatchTo(scrollOnNewMessageCallback) }
|
||||
timelineEventController.addModelBuildListener {
|
||||
it.dispatchTo(stateRestorer)
|
||||
it.dispatchTo(scrollOnNewMessageCallback)
|
||||
}
|
||||
recyclerView.setController(timelineEventController)
|
||||
timelineEventController.callback = this
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.riotredesign.features.home.room.detail
|
|||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
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.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
@ -46,7 +45,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
|
|||
|
||||
@JvmStatic
|
||||
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>()
|
||||
return RoomDetailViewModel(state, currentSession, visibleRoomHolder)
|
||||
}
|
||||
|
|
|
@ -16,13 +16,18 @@
|
|||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.util.Linkify
|
||||
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.*
|
||||
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.riotredesign.R
|
||||
import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
|
||||
|
@ -146,7 +151,7 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
.informationData(informationData)
|
||||
}
|
||||
|
||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence {
|
||||
private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): Spannable {
|
||||
val spannable = SpannableStringBuilder(body)
|
||||
MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback {
|
||||
override fun onUrlClicked(url: String) {
|
||||
|
|
|
@ -16,30 +16,54 @@
|
|||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline
|
||||
|
||||
import android.text.Spannable
|
||||
import android.widget.ImageView
|
||||
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.EpoxyModelClass
|
||||
import im.vector.matrix.android.api.permalinks.MatrixLinkify
|
||||
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)
|
||||
abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute var message: CharSequence? = null
|
||||
@EpoxyAttribute var message: Spannable? = null
|
||||
@EpoxyAttribute override lateinit var informationData: MessageInformationData
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.messageView.text = message
|
||||
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() {
|
||||
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 messageView by bind<TextView>(R.id.messageTextView)
|
||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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.paging.PagedListEpoxyController
|
||||
|
||||
class TimelineEventController(private val roomId: String,
|
||||
private val dateFormatter: TimelineDateFormatter,
|
||||
class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
||||
private val timelineItemFactory: TimelineItemFactory,
|
||||
private val timelineMediaSizeProvider: TimelineMediaSizeProvider
|
||||
) : PagedListEpoxyController<TimelineEvent>(
|
||||
|
@ -82,7 +81,7 @@ class TimelineEventController(private val roomId: String,
|
|||
}
|
||||
if (addDaySeparator) {
|
||||
val formattedDay = dateFormatter.formatMessageDay(date)
|
||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(roomId + formattedDay)
|
||||
val daySeparatorItem = DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
|
||||
epoxyModels.add(daySeparatorItem)
|
||||
}
|
||||
return epoxyModels
|
||||
|
@ -90,13 +89,13 @@ class TimelineEventController(private val roomId: String,
|
|||
|
||||
override fun addModels(models: List<EpoxyModel<*>>) {
|
||||
LoadingItemModel_()
|
||||
.id(roomId + "forward_loading_item")
|
||||
.id("forward_loading_item")
|
||||
.addIf(isLoadingForward, this)
|
||||
|
||||
super.add(models)
|
||||
|
||||
LoadingItemModel_()
|
||||
.id(roomId + "backward_loading_item")
|
||||
.id("backward_loading_item")
|
||||
.addIf(!hasReachedEnd, this)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,8 +22,8 @@ sealed class RoomListActions {
|
|||
|
||||
data class SelectRoom(val roomSummary: RoomSummary) : RoomListActions()
|
||||
|
||||
object RoomDisplayed : RoomListActions()
|
||||
|
||||
data class FilterRooms(val roomName: CharSequence? = null) : RoomListActions()
|
||||
|
||||
data class ToggleCategory(val category: RoomCategory) : RoomListActions()
|
||||
|
||||
}
|
|
@ -22,19 +22,25 @@ import android.text.TextWatcher
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Incomplete
|
||||
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.session.room.model.RoomSummary
|
||||
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.platform.RiotFragment
|
||||
import im.vector.riotredesign.core.platform.StateView
|
||||
import im.vector.riotredesign.features.home.HomeModule
|
||||
import im.vector.riotredesign.features.home.HomeNavigator
|
||||
import kotlinx.android.synthetic.main.fragment_room_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 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 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? {
|
||||
return inflater.inflate(R.layout.fragment_room_list, container, false)
|
||||
|
@ -54,11 +60,36 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
|||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
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.addModelBuildListener { it.dispatchTo(stateRestorer) }
|
||||
stateView.contentView = epoxyRecyclerView
|
||||
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) {
|
||||
|
@ -90,24 +121,13 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
|
|||
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 **************************************************************
|
||||
|
||||
override fun onRoomSelected(room: RoomSummary) {
|
||||
homeViewModel.accept(RoomListActions.SelectRoom(room))
|
||||
homeNavigator.openRoomDetail(room.roomId, null)
|
||||
roomListViewModel.accept(RoomListActions.SelectRoom(room))
|
||||
}
|
||||
|
||||
override fun onToggleRoomCategory(roomCategory: RoomCategory) {
|
||||
roomListViewModel.accept(RoomListActions.ToggleCategory(roomCategory))
|
||||
}
|
||||
}
|
|
@ -16,22 +16,23 @@
|
|||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
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.group.model.GroupSummary
|
||||
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.rx.rx
|
||||
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.room.VisibleRoomStore
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import org.koin.android.ext.android.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
@ -49,7 +50,7 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||
|
||||
@JvmStatic
|
||||
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 selectedGroupHolder = viewModelContext.activity.get<SelectedGroupStore>()
|
||||
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 _openRoomLiveData = MutableLiveData<LiveEvent<String>>()
|
||||
val openRoomLiveData: LiveData<LiveEvent<String>>
|
||||
get() = _openRoomLiveData
|
||||
|
||||
init {
|
||||
observeRoomSummaries()
|
||||
observeVisibleRoom()
|
||||
|
@ -68,16 +73,18 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||
|
||||
fun accept(action: RoomListActions) {
|
||||
when (action) {
|
||||
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
||||
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
||||
is RoomListActions.SelectRoom -> handleSelectRoom(action)
|
||||
is RoomListActions.FilterRooms -> handleFilterRooms(action)
|
||||
is RoomListActions.ToggleCategory -> handleToggleCategory(action)
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
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)
|
||||
_openRoomLiveData.postValue(LiveEvent(action.roomSummary.roomId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,10 +93,14 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||
roomListFilter.accept(optionalFilter)
|
||||
}
|
||||
|
||||
private fun handleToggleCategory(action: RoomListActions.ToggleCategory) = setState {
|
||||
this.toggle(action.category)
|
||||
}
|
||||
|
||||
private fun observeVisibleRoom() {
|
||||
visibleRoomHolder.observe()
|
||||
.doOnNext {
|
||||
setState { copy(selectedRoomId = it) }
|
||||
setState { copy(visibleRoomId = it) }
|
||||
}
|
||||
.subscribe()
|
||||
.disposeOnClear()
|
||||
|
@ -159,13 +170,13 @@ class RoomListViewModel(initialState: RoomListViewState,
|
|||
}
|
||||
}
|
||||
|
||||
return RoomSummaries(
|
||||
favourites = favourites.sortedWith(roomSummaryComparator),
|
||||
directRooms = directChats.sortedWith(roomSummaryComparator),
|
||||
groupRooms = groupRooms.sortedWith(roomSummaryComparator),
|
||||
lowPriorities = lowPriorities.sortedWith(roomSummaryComparator),
|
||||
serverNotices = serverNotices.sortedWith(roomSummaryComparator)
|
||||
)
|
||||
return RoomSummaries().apply {
|
||||
put(RoomCategory.FAVOURITE, favourites.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.DIRECT, directChats.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.GROUP, groupRooms.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.LOW_PRIORITY, lowPriorities.sortedWith(roomSummaryComparator))
|
||||
put(RoomCategory.SERVER_NOTICE, serverNotices.sortedWith(roomSummaryComparator))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -16,24 +16,54 @@
|
|||
|
||||
package im.vector.riotredesign.features.home.room.list
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
|
||||
data class RoomListViewState(
|
||||
val asyncRooms: Async<RoomSummaries> = Uninitialized,
|
||||
val selectedRoomId: String? = null
|
||||
) : MvRxState
|
||||
val visibleRoomId: String? = null,
|
||||
val isFavouriteRoomsExpanded: Boolean = true,
|
||||
val isDirectRoomsExpanded: Boolean = false,
|
||||
val isGroupRoomsExpanded: Boolean = false,
|
||||
val isLowPriorityRoomsExpanded: Boolean = false,
|
||||
val isServerNoticeRoomsExpanded: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
data class RoomSummaries(
|
||||
val favourites: List<RoomSummary>,
|
||||
val directRooms: List<RoomSummary>,
|
||||
val groupRooms: List<RoomSummary>,
|
||||
val lowPriorities: List<RoomSummary>,
|
||||
val serverNotices: List<RoomSummary>
|
||||
)
|
||||
fun isCategoryExpanded(roomCategory: RoomCategory): Boolean {
|
||||
return when (roomCategory) {
|
||||
RoomCategory.FAVOURITE -> isFavouriteRoomsExpanded
|
||||
RoomCategory.DIRECT -> isDirectRoomsExpanded
|
||||
RoomCategory.GROUP -> isGroupRoomsExpanded
|
||||
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 {
|
||||
return this == null || (directRooms.isEmpty() && groupRooms.isEmpty() && favourites.isEmpty() && lowPriorities.isEmpty() && serverNotices.isEmpty())
|
||||
return this == null || isEmpty()
|
||||
}
|
|
@ -19,65 +19,34 @@ package im.vector.riotredesign.features.home.room.list
|
|||
import androidx.annotation.StringRes
|
||||
import com.airbnb.epoxy.TypedEpoxyController
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.resources.StringProvider
|
||||
|
||||
class RoomSummaryController(private val stringProvider: StringProvider
|
||||
) : 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
|
||||
|
||||
override fun buildModels(viewState: RoomListViewState) {
|
||||
val roomSummaries = viewState.asyncRooms()
|
||||
val favourites = roomSummaries?.favourites ?: emptyList()
|
||||
buildRoomCategory(viewState, favourites, R.string.room_list_favourites, isFavoriteRoomsExpanded) {
|
||||
isFavoriteRoomsExpanded = !isFavoriteRoomsExpanded
|
||||
roomSummaries?.forEach { (category, summaries) ->
|
||||
if (summaries.isEmpty()) {
|
||||
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) {
|
||||
if (summaries.isEmpty()) {
|
||||
return
|
||||
}
|
||||
//TODO should add some business logic later
|
||||
val unreadCount = if (summaries.isEmpty()) {
|
||||
0
|
||||
|
@ -117,6 +86,7 @@ class RoomSummaryController(private val stringProvider: StringProvider
|
|||
}
|
||||
|
||||
interface Callback {
|
||||
fun onToggleRoomCategory(roomCategory: RoomCategory)
|
||||
fun onRoomSelected(room: RoomSummary)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
package im.vector.riotredesign.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.text.style.ImageSpan
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkData
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkParser
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.core.glide.GlideRequests
|
||||
import org.commonmark.node.BlockQuote
|
||||
import org.commonmark.node.HtmlBlock
|
||||
import org.commonmark.node.HtmlInline
|
||||
|
@ -49,11 +49,12 @@ import ru.noties.markwon.html.tag.SuperScriptHandler
|
|||
import ru.noties.markwon.html.tag.UnderlineHandler
|
||||
import java.util.Arrays.asList
|
||||
|
||||
class EventHtmlRenderer(private val context: Context,
|
||||
private val session: Session) {
|
||||
class EventHtmlRenderer(glideRequests: GlideRequests,
|
||||
context: Context,
|
||||
session: Session) {
|
||||
|
||||
private val markwon = Markwon.builder(context)
|
||||
.usePlugin(MatrixPlugin.create(context, session))
|
||||
.usePlugin(MatrixPlugin.create(glideRequests, context, session))
|
||||
.build()
|
||||
|
||||
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() {
|
||||
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
|
@ -76,7 +78,7 @@ private class MatrixPlugin private constructor(private val context: Context,
|
|||
ImageHandler.create())
|
||||
.addHandler(
|
||||
"a",
|
||||
MxLinkHandler(context, session))
|
||||
MxLinkHandler(glideRequests, context, session))
|
||||
.addHandler(
|
||||
"blockquote",
|
||||
BlockquoteHandler())
|
||||
|
@ -128,13 +130,15 @@ private class MatrixPlugin private constructor(private val context: Context,
|
|||
|
||||
companion object {
|
||||
|
||||
fun create(context: Context, session: Session): MatrixPlugin {
|
||||
return MatrixPlugin(context, session)
|
||||
fun create(glideRequests: GlideRequests, context: Context, session: Session): MatrixPlugin {
|
||||
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()
|
||||
|
||||
|
@ -144,9 +148,8 @@ private class MxLinkHandler(private val context: Context, private val session: S
|
|||
val permalinkData = PermalinkParser.parse(link)
|
||||
when (permalinkData) {
|
||||
is PermalinkData.UserLink -> {
|
||||
val user = session.getUser(permalinkData.userId) ?: return
|
||||
val drawable = PillDrawableFactory.create(context, permalinkData.userId, user)
|
||||
val span = ImageSpan(drawable)
|
||||
val user = session.getUser(permalinkData.userId)
|
||||
val span = PillImageSpan(glideRequests, context, permalinkData.userId, user)
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
span,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,6 @@
|
|||
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" />
|
||||
|
||||
|
@ -24,14 +23,15 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="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"
|
||||
app:layout_constraintTop_toTopOf="@+id/itemNoticeAvatarView"
|
||||
tools:text="Mon item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue