diff --git a/.idea/dictionaries/ganfra.xml b/.idea/dictionaries/ganfra.xml index 7e1fdcdd..728c63d6 100644 --- a/.idea/dictionaries/ganfra.xml +++ b/.idea/dictionaries/ganfra.xml @@ -3,9 +3,14 @@ connectable coroutine + linkify + markon + markwon 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..9a82a20f 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 @@ -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) { 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/AvatarRenderer.kt b/app/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 3227dc65..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 @@ -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 + */ 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) { 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 { - val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) - return GlideApp - .with(context) + // 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) } - 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?) } } \ 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..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,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() } 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..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 @@ -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 { parametersOf(roomDetailArgs.roomId) } - private val homePermalinkHandler by inject() 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 } 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/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..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 @@ -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() { - @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? = 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/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) } 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..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 @@ -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, 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..3f48043d --- /dev/null +++ b/app/src/main/java/im/vector/riotredesign/features/html/PillImageSpan.kt @@ -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? = 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() { + + 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 { + updateAvatarDrawable(drawable) + } + } +} \ No newline at end of file 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