From fd3fce6deb978d65c4fee89a5d9301c956a0cf02 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Feb 2019 18:50:30 +0100 Subject: [PATCH 1/4] State issues : restore recyclerview state + fix DI issues --- .idea/dictionaries/ganfra.xml | 2 + .../main/java/im/vector/riotredesign/Riot.kt | 6 +- .../vector/riotredesign/core/di/AppModule.kt | 21 +++ .../core/epoxy/LayoutManagerStateRestorer.kt | 48 +++++++ .../riotredesign/core/platform/Restorable.kt | 27 ++++ .../core/platform/RiotActivity.kt | 21 +++ .../core/platform/RiotFragment.kt | 21 +++ .../features/home/HomeActivity.kt | 8 +- .../riotredesign/features/home/HomeModule.kt | 134 +++++++++--------- .../features/home/HomeNavigator.kt | 3 - .../features/home/group/GroupListFragment.kt | 12 +- .../features/home/group/GroupListViewModel.kt | 3 +- .../home/group/GroupSummaryController.kt | 5 +- .../home/room/detail/RoomDetailFragment.kt | 21 ++- .../home/room/detail/RoomDetailViewModel.kt | 3 +- .../timeline/TimelineEventController.kt | 9 +- .../home/room/list/RoomListActions.kt | 4 +- .../home/room/list/RoomListFragment.kt | 60 +++++--- .../home/room/list/RoomListViewModel.kt | 39 +++-- .../home/room/list/RoomListViewState.kt | 50 +++++-- .../home/room/list/RoomSummaryController.kt | 62 +++----- 21 files changed, 371 insertions(+), 188 deletions(-) create mode 100644 app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt create mode 100644 app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index 7e1fdcdd..c3f99f8d 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -6,6 +6,8 @@ merlins moshi persistor + restorable + restorables synchronizer untimelined diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index 8385443c..acc59cde 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -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 @@ -32,12 +33,15 @@ class Riot : Application() { override fun onCreate() { super.onCreate() + applicationContext.setTheme(R.style.Theme_Riot) if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) Stetho.initializeWithDefaults(this) } AndroidThreeTen.init(this) - startKoin(listOf(AppModule(this).definition), logger = EmptyLogger()) + val appModule = AppModule(applicationContext).definition + val homeModule = HomeModule(applicationContext).definition + startKoin(listOf(appModule, homeModule), logger = EmptyLogger()) } override fun attachBaseContext(base: Context) { diff --git a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt index bbc97315..f67c9c11 100644 --- a/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt +++ b/app/src/main/java/im/vector/riotredesign/core/di/AppModule.kt @@ -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 + } + + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt b/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt new file mode 100644 index 00000000..b4b9f181 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/epoxy/LayoutManagerStateRestorer.kt @@ -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() + + override fun onSaveInstanceState(outState: Bundle) { + val layoutManagerState = layoutManager.onSaveInstanceState() + outState.putParcelable(LAYOUT_MANAGER_STATE, layoutManagerState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle?) { + val parcelable = savedInstanceState?.getParcelable(LAYOUT_MANAGER_STATE) + layoutManagerState.set(parcelable) + } + + override fun onInserted(position: Int, count: Int) { + layoutManagerState.getAndSet(null)?.also { + layoutManager.onRestoreInstanceState(it) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt b/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt new file mode 100644 index 00000000..683fed73 --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/core/platform/Restorable.kt @@ -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?) + +} \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt index 2465399d..db1aa476 100644 --- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotActivity.kt @@ -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() + + 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.register(): T { + Util.assertMainThread() + restorables.add(this) + return this + } protected fun Disposable.disposeOnDestroy(): Disposable { uiDisposables.add(this) diff --git a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt index 72a7f4b2..0b7996fa 100644 --- a/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/core/platform/RiotFragment.kt @@ -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() + + 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.register(): T { + assertMainThread() + restorables.add(this) + return this + } + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt index 12842034..db37dfc0 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeActivity.kt @@ -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() @@ -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() diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 28627657..71d9f94c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -16,7 +16,8 @@ package im.vector.riotredesign.features.home -import im.vector.matrix.android.api.Matrix +import android.content.Context +import im.vector.riotredesign.features.home.group.GroupSummaryController import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory @@ -35,84 +36,83 @@ 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(context: Context) { - 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 { + // Activity scope + + scope(HOME_SCOPE) { TimelineDateFormatter(get()) } - 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() + scope(HOME_SCOPE) { + RoomNameItemFactory(get()) + } + + scope(HOME_SCOPE) { + RoomTopicItemFactory(get()) + } + + scope(HOME_SCOPE) { + RoomMemberItemFactory(get()) + } + + scope(HOME_SCOPE) { + CallItemFactory(get()) + } + + scope(HOME_SCOPE) { + RoomHistoryVisibilityItemFactory(get()) + } + + scope(HOME_SCOPE) { + DefaultItemFactory() + } + + scope(HOME_SCOPE) { + TimelineMediaSizeProvider() + } + + scope(HOME_SCOPE) { + EventHtmlRenderer(context, get()) + } + + scope(HOME_SCOPE) { + MessageItemFactory(get(), get(), get(), get()) + } + + scope(HOME_SCOPE) { + TimelineItemFactory(get(), get(), get(), get(), get(), get(), get()) + } + + // Fragment scopes + + scope(ROOM_DETAIL_SCOPE) { + TimelineEventController(get(), get(), get()) + } + + scope(ROOM_LIST_SCOPE) { + RoomSummaryController(get()) + } + + scope(GROUP_LIST_SCOPE) { + GroupSummaryController() } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt index 4fb84ea1..f6583b97 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeNavigator.kt @@ -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) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt index 30b6a6d0..fb99e839 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListFragment.kt @@ -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() 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) } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt index f35c46fc..1ab01369 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupListViewModel.kt @@ -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() val selectedGroupHolder = viewModelContext.activity.get() return GroupListViewModel(state, selectedGroupHolder, currentSession) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt index 3f24f9e0..0fc0c9d4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/group/GroupSummaryController.kt @@ -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() { +class GroupSummaryController : TypedEpoxyController() { + + var callback: Callback? = null override fun buildModels(viewState: GroupListViewState) { buildGroupModels(viewState.asyncGroups(), viewState.selectedGroup) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 76f91c7b..1e151134 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -25,19 +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.core.parameter.parametersOf +import org.koin.android.scope.ext.android.bindScope +import org.koin.android.scope.ext.android.getOrCreateScope @Parcelize data class RoomDetailArgs( @@ -45,6 +47,7 @@ data class RoomDetailArgs( val eventId: String? = null ) : Parcelable + class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { companion object { @@ -57,10 +60,9 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() - private val roomDetailArgs: RoomDetailArgs by args() - - private val timelineEventController by inject { parametersOf(roomDetailArgs.roomId) } + private val timelineEventController by inject() private val homePermalinkHandler by inject() + private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -69,6 +71,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 +83,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { roomDetailViewModel.process(RoomDetailActions.IsDisplayed) } + // PRIVATE METHODS ***************************************************************************** + private fun setupToolbar() { val parentActivity = riotActivity if (parentActivity is ToolbarConfigurable) { @@ -91,10 +96,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 } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 3ce30c0e..4ca21b94 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -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() val visibleRoomHolder = viewModelContext.activity.get() return RoomDetailViewModel(state, currentSession, visibleRoomHolder) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index ec865e4b..883476de 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -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( @@ -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>) { 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) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt index efc8b664..dc9c393b 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListActions.kt @@ -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() + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt index 904cf66c..deeba711 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListFragment.kt @@ -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() private val roomController by inject() - private val homeViewModel: RoomListViewModel by activityViewModel() + private val homeNavigator by inject() + 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)) + } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt index 0612016c..f5213432 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewModel.kt @@ -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() val roomSelectionRepository = viewModelContext.activity.get() val selectedGroupHolder = viewModelContext.activity.get() val visibleRoomHolder = viewModelContext.activity.get() @@ -61,6 +62,10 @@ class RoomListViewModel(initialState: RoomListViewState, private val roomListFilter = BehaviorRelay.createDefault>(Option.empty()) + private val _openRoomLiveData = MutableLiveData>() + val openRoomLiveData: LiveData> + 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)) + } } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt index 57d0be76..7466618e 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomListViewState.kt @@ -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 = 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, - val directRooms: List, - val groupRooms: List, - val lowPriorities: List, - val serverNotices: List -) + 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> + +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() } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt index 8178e2b8..de850df5 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/list/RoomSummaryController.kt @@ -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() { - 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, @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) } From 3d7562ea8eabd48a258e82ce6d4c7ebe1c44b5bc Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 28 Feb 2019 18:50:46 +0100 Subject: [PATCH 2/4] UI : fix notice avatar position --- app/src/main/res/layout/item_timeline_event_notice.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/item_timeline_event_notice.xml b/app/src/main/res/layout/item_timeline_event_notice.xml index 2574940f..8644ac82 100644 --- a/app/src/main/res/layout/item_timeline_event_notice.xml +++ b/app/src/main/res/layout/item_timeline_event_notice.xml @@ -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" /> \ No newline at end of file From fffdf4b8c1d45b3950050ae00e2502aac5f55a0d Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 1 Mar 2019 21:44:26 +0100 Subject: [PATCH 3/4] Pills : try to optimize memory and get better perf. Still need to rework a bit. --- .idea/dictionaries/ganfra.xml | 3 + .../main/java/im/vector/riotredesign/Riot.kt | 3 +- .../features/home/AvatarRenderer.kt | 48 +++---- .../riotredesign/features/home/HomeModule.kt | 69 +++------- .../home/room/detail/RoomDetailFragment.kt | 5 +- .../detail/timeline/MessageItemFactory.kt | 9 +- .../room/detail/timeline/MessageTextItem.kt | 35 ++++- .../features/html/EventHtmlRenderer.kt | 25 ++-- .../features/html/PillDrawableFactory.kt | 59 -------- .../features/html/PillImageSpan.kt | 129 ++++++++++++++++++ 10 files changed, 224 insertions(+), 161 deletions(-) delete mode 100644 app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt create mode 100644 app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index c3f99f8d..728c63d6 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -3,6 +3,9 @@ connectable coroutine + linkify + markon + markwon merlins moshi persistor diff --git a/app/src/main/java/im/vector/riotredesign/Riot.kt b/app/src/main/java/im/vector/riotredesign/Riot.kt index acc59cde..9a82a20f 100644 --- a/app/src/main/java/im/vector/riotredesign/Riot.kt +++ b/app/src/main/java/im/vector/riotredesign/Riot.kt @@ -33,14 +33,13 @@ class Riot : Application() { override fun onCreate() { super.onCreate() - applicationContext.setTheme(R.style.Theme_Riot) if (BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) Stetho.initializeWithDefaults(this) } AndroidThreeTen.init(this) val appModule = AppModule(applicationContext).definition - val homeModule = HomeModule(applicationContext).definition + val homeModule = HomeModule().definition startKoin(listOf(appModule, homeModule), logger = EmptyLogger()) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 3227dc65..267d1a3a 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -19,6 +19,7 @@ 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.core.content.ContextCompat import com.amulyakhare.textdrawable.TextDrawable import com.bumptech.glide.request.RequestOptions @@ -29,64 +30,49 @@ 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 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) { if (name.isNullOrEmpty()) { return } val placeholder = buildPlaceholderDrawable(imageView.context, name) - buildGlideRequest(imageView.context, avatarUrl) + buildGlideRequest(GlideApp.with(imageView), avatarUrl) .placeholder(placeholder) .into(imageView) } - 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) - } - } - } - - private fun buildGlideRequest(context: Context, avatarUrl: String?): GlideRequest { + fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) - return GlideApp - .with(context) + return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) } - private fun buildPlaceholderDrawable(context: Context, name: String): Drawable { + 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?) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 71d9f94c..04a93d66 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -16,10 +16,9 @@ package im.vector.riotredesign.features.home -import android.content.Context +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.group.SelectedGroupStore -import im.vector.riotredesign.features.home.room.VisibleRoomStore 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 @@ -31,12 +30,11 @@ 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(context: Context) { +class HomeModule { companion object { const val HOME_SCOPE = "HOME_SCOPE" @@ -49,10 +47,6 @@ class HomeModule(context: Context) { // Activity scope - scope(HOME_SCOPE) { - TimelineDateFormatter(get()) - } - scope(HOME_SCOPE) { HomeNavigator() } @@ -61,50 +55,23 @@ class HomeModule(context: Context) { HomePermalinkHandler(get()) } - scope(HOME_SCOPE) { - RoomNameItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomTopicItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomMemberItemFactory(get()) - } - - scope(HOME_SCOPE) { - CallItemFactory(get()) - } - - scope(HOME_SCOPE) { - RoomHistoryVisibilityItemFactory(get()) - } - - scope(HOME_SCOPE) { - DefaultItemFactory() - } - - scope(HOME_SCOPE) { - TimelineMediaSizeProvider() - } - - scope(HOME_SCOPE) { - EventHtmlRenderer(context, get()) - } - - scope(HOME_SCOPE) { - MessageItemFactory(get(), get(), get(), get()) - } - - scope(HOME_SCOPE) { - TimelineItemFactory(get(), get(), get(), get(), get(), get(), get()) - } - // Fragment scopes - scope(ROOM_DETAIL_SCOPE) { - TimelineEventController(get(), get(), get()) + 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) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 1e151134..faf792d7 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -40,6 +40,7 @@ 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 data class RoomDetailArgs( @@ -60,8 +61,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } private val roomDetailViewModel: RoomDetailViewModel by fragmentViewModel() - private val timelineEventController by inject() - private val homePermalinkHandler by inject() + private val timelineEventController: TimelineEventController by inject { parametersOf(this) } + private val homePermalinkHandler: HomePermalinkHandler by inject() private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt index d27475f7..b31ccf94 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageItemFactory.kt @@ -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) { diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt index 8265313d..49c754c4 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt @@ -16,30 +16,59 @@ 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() { - @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) } + } + + override fun unbind(holder: Holder) { + findPillsAndProcess { it.unbind() } + super.unbind(holder) + } + + private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { + GlobalScope.launch(Dispatchers.Main) { + val pillImageSpans: Array? = 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(R.id.messageAvatarImageView) override val memberNameView by bind(R.id.messageMemberNameView) override val timeView by bind(R.id.messageTimeView) - val messageView by bind(R.id.messageTextView) + val messageView by bind(R.id.messageTextView) } diff --git a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt index dcbe28f1..27e00b54 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt @@ -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() @@ -145,8 +149,7 @@ private class MxLinkHandler(private val context: Context, private val session: S 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 span = PillImageSpan(glideRequests, context, permalinkData.userId, user) SpannableBuilder.setSpans( visitor.builder(), span, diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt deleted file mode 100644 index 9b87c671..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/html/PillDrawableFactory.kt +++ /dev/null @@ -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) - - override fun onDrawableUpdated(drawable: Drawable?) { - weakChipDrawable.get()?.apply { - chipIcon = drawable - setBounds(0, 0, intrinsicWidth, intrinsicHeight) - } - } - - } - -} - diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt new file mode 100644 index 00000000..cbc0251a --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -0,0 +1,129 @@ +/* + * 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.MainThread +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 + +class PillImageSpan(private val glideRequests: GlideRequests, + private val context: Context, + private val userId: String, + private val user: User?) : ReplacementSpan() { + + private val pillDrawable = createChipDrawable(context, userId, user) + private val target = PillImageSpanTarget(this) + private var tv: WeakReference? = null + + @MainThread + fun bind(textView: TextView) { + tv = WeakReference(textView) + AvatarRenderer.buildGlideRequest(glideRequests, user?.avatarUrl).into(target) + } + + @MainThread + fun unbind() { + glideRequests.clear(target) + tv = null + } + + @MainThread + private fun updateAvatarDrawable(drawable: Drawable?) { + pillDrawable.apply { + chipIcon = drawable + } + tv?.get()?.apply { + invalidate() + } + } + + 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() + } + + private fun createChipDrawable(context: Context, userId: String, user: User?): ChipDrawable { + val textPadding = context.resources.getDimension(R.dimen.pill_text_padding) + val displayName = if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! + 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.buildPlaceholderDrawable(context, displayName) + setBounds(0, 0, intrinsicWidth, intrinsicHeight) + } + } + + private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget() { + + private val pillImageSpan = WeakReference(pillImageSpan) + + override fun onResourceReady(drawable: Drawable, transition: Transition?) { + updateWith(drawable) + } + + override fun onLoadCleared(placeholder: Drawable?) { + updateWith(placeholder) + } + + private fun updateWith(drawable: Drawable?) { + pillImageSpan.get()?.apply { + this.updateAvatarDrawable(drawable) + } + } + } + +} \ No newline at end of file From ef3fb561e986febdeb377b47e175491e91c7ceed Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 4 Mar 2019 16:52:44 +0100 Subject: [PATCH 4/4] Pills : finalize avatar retrieval --- .../features/home/AvatarRenderer.kt | 50 +++++++++-- .../room/detail/timeline/MessageTextItem.kt | 5 -- .../features/html/EventHtmlRenderer.kt | 2 +- .../features/html/PillImageSpan.kt | 83 ++++++++++--------- 4 files changed, 91 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 267d1a3a..275d7613 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -20,11 +20,16 @@ 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 @@ -32,6 +37,9 @@ import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideRequest import im.vector.riotredesign.core.glide.GlideRequests +/** + * This helper centralise ways to retrieve avatar into ImageView or even generic Target + */ object AvatarRenderer { @UiThread @@ -46,23 +54,53 @@ object AvatarRenderer { @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) { if (name.isNullOrEmpty()) { return } - val placeholder = buildPlaceholderDrawable(imageView.context, name) - buildGlideRequest(GlideApp.with(imageView), avatarUrl) + val placeholder = buildPlaceholderDrawable(context, name) + buildGlideRequest(glideRequest, avatarUrl, size) .placeholder(placeholder) - .into(imageView) + .into(target) } - fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest { - val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) + @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 API ********************************************************************************* + + private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?, size: Int): GlideRequest { + val resolvedUrl = Matrix.getInstance().currentSession + .contentUrlResolver() + .resolveThumbnail(avatarUrl, size, size, ContentUrlResolver.ThumbnailMethod.SCALE) + return glideRequest .load(resolvedUrl) .apply(RequestOptions.circleCropTransform()) + .diskCacheStrategy(DiskCacheStrategy.DATA) } - fun buildPlaceholderDrawable(context: Context, text: String): Drawable { + private fun buildPlaceholderDrawable(context: Context, text: String): Drawable { val avatarColor = ContextCompat.getColor(context, R.color.pale_teal) return if (text.isEmpty()) { TextDrawable.builder().buildRound("", avatarColor) diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt index 49c754c4..9a0bc8ba 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/MessageTextItem.kt @@ -48,11 +48,6 @@ abstract class MessageTextItem : AbsMessageItem() { findPillsAndProcess { it.bind(holder.messageView) } } - override fun unbind(holder: Holder) { - findPillsAndProcess { it.unbind() } - super.unbind(holder) - } - private fun findPillsAndProcess(processBlock: (span: PillImageSpan) -> Unit) { GlobalScope.launch(Dispatchers.Main) { val pillImageSpans: Array? = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt index 27e00b54..2ecee795 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/EventHtmlRenderer.kt @@ -148,7 +148,7 @@ private class MxLinkHandler(private val glideRequests: GlideRequests, val permalinkData = PermalinkParser.parse(link) when (permalinkData) { is PermalinkData.UserLink -> { - val user = session.getUser(permalinkData.userId) ?: return + val user = session.getUser(permalinkData.userId) val span = PillImageSpan(glideRequests, context, permalinkData.userId, user) SpannableBuilder.setSpans( visitor.builder(), diff --git a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt index cbc0251a..3f48043d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -22,7 +22,7 @@ import android.graphics.Paint import android.graphics.drawable.Drawable import android.text.style.ReplacementSpan import android.widget.TextView -import androidx.annotation.MainThread +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 @@ -32,36 +32,33 @@ 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 pillDrawable = createChipDrawable(context, userId, user) + 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? = null - @MainThread + @UiThread fun bind(textView: TextView) { tv = WeakReference(textView) - AvatarRenderer.buildGlideRequest(glideRequests, user?.avatarUrl).into(target) + AvatarRenderer.render(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE, target) } - @MainThread - fun unbind() { - glideRequests.clear(target) - tv = null - } - - @MainThread - private fun updateAvatarDrawable(drawable: Drawable?) { - pillDrawable.apply { - chipIcon = drawable - } - tv?.get()?.apply { - invalidate() - } - } + // ReplacementSpan ***************************************************************************** override fun getSize(paint: Paint, text: CharSequence, start: Int, @@ -85,7 +82,6 @@ class PillImageSpan(private val glideRequests: GlideRequests, y: Int, bottom: Int, paint: Paint) { - canvas.save() val transY = bottom - pillDrawable.bounds.bottom canvas.translate(x, transY.toFloat()) @@ -93,37 +89,50 @@ class PillImageSpan(private val glideRequests: GlideRequests, canvas.restore() } - private fun createChipDrawable(context: Context, userId: String, user: User?): ChipDrawable { + 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) - val displayName = if (user?.displayName.isNullOrEmpty()) userId else user?.displayName!! 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.buildPlaceholderDrawable(context, displayName) + chipIcon = AvatarRenderer.getCachedOrPlaceholder(context, glideRequests, user?.avatarUrl, displayName, PILL_AVATAR_SIZE) setBounds(0, 0, intrinsicWidth, intrinsicHeight) } } - private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget() { +} - private val pillImageSpan = WeakReference(pillImageSpan) +/** + * Glide target to handle avatar retrieval into [PillImageSpan]. + */ +private class PillImageSpanTarget(pillImageSpan: PillImageSpan) : SimpleTarget() { - override fun onResourceReady(drawable: Drawable, transition: Transition?) { - updateWith(drawable) - } + private val pillImageSpan = WeakReference(pillImageSpan) - override fun onLoadCleared(placeholder: Drawable?) { - updateWith(placeholder) - } - - private fun updateWith(drawable: Drawable?) { - pillImageSpan.get()?.apply { - this.updateAvatarDrawable(drawable) - } - } + override fun onResourceReady(drawable: Drawable, transition: Transition?) { + updateWith(drawable) } + override fun onLoadCleared(placeholder: Drawable?) { + updateWith(placeholder) + } + + private fun updateWith(drawable: Drawable?) { + pillImageSpan.get()?.apply { + updateAvatarDrawable(drawable) + } + } } \ No newline at end of file