From 5bf1761f2734f4155916edbdb5fc60fb486f1de0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Sep 2021 18:31:17 +0200 Subject: [PATCH 1/6] Room detail: use StubView for InviteView --- .../im/vector/app/core/extensions/ViewStub.kt | 26 +++++++++++++++++++ .../home/room/detail/RoomDetailFragment.kt | 25 +++++++++++------- .../timeline/TimelineEventController.kt | 6 ++++- .../main/res/layout/fragment_room_detail.xml | 13 +++++----- .../res/layout/view_stub_invite_layout.xml | 5 ++++ 5 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt create mode 100644 vector/src/main/res/layout/view_stub_invite_layout.xml diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt new file mode 100644 index 0000000000..33b3f742cc --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt @@ -0,0 +1,26 @@ +/* + * 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.app.core.extensions + +import android.view.View +import android.view.ViewStub + +inline fun ViewStub.inflateIfNeeded(onInflate: (T) -> Unit) { + if (parent != null) { + onInflate(inflate() as T) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 287ff70dde..d19df302f0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -76,6 +76,7 @@ import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.extensions.inflateIfNeeded import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showKeyboard @@ -312,6 +313,7 @@ class RoomDetailFragment @Inject constructor( private var lockSendButton = false private val currentCallsViewPresenter = CurrentCallsViewPresenter() + private var inviteView: VectorInviteView? = null private lateinit var emojiPopup: EmojiPopup override fun onCreate(savedInstanceState: Bundle?) { @@ -343,7 +345,6 @@ class RoomDetailFragment @Inject constructor( setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() - setupInviteView() setupNotificationView() setupJumpToReadMarkerView() setupActiveCallView() @@ -1350,10 +1351,6 @@ class RoomDetailFragment @Inject constructor( return isHandled } - private fun setupInviteView() { - views.inviteView.callback = this - } - override fun invalidate() = withState(roomDetailViewModel) { state -> invalidateOptionsMenu() val summary = state.asyncRoomSummary() @@ -1365,7 +1362,7 @@ class RoomDetailFragment @Inject constructor( views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages timelineEventController.update(state) - views.inviteView.isVisible = false + inviteView?.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { @@ -1386,10 +1383,18 @@ class RoomDetailFragment @Inject constructor( views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.inviteView.isVisible = true - views.inviteView.render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) - // Intercept click event - views.inviteView.setOnClickListener { } + views.inviteViewStub.inflateIfNeeded { + inviteView = it + } + views.composerLayout.isVisible = false + views.voiceMessageRecorderView.isVisible = false + inviteView?.apply { + callback = this@RoomDetailFragment + isVisible = true + render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) + setOnClickListener { } + } + Unit } else if (state.asyncInviter.complete) { vectorBaseActivity.finish() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 8be319f2a8..39b3cd5061 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -39,13 +39,13 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams -import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener +import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem @@ -276,6 +276,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } override fun buildModels() { + // Don't build anything if membership is not joined + if (partialState.roomSummary?.membership != Membership.JOIN) { + return + } val timestamp = System.currentTimeMillis() val showingForwardLoader = LoadingItem_() diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index be4559d009..482cdbc700 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -180,7 +180,7 @@ - + app:layout_constraintTop_toBottomOf="@+id/appBarLayout" /> + \ No newline at end of file From fb10e9f113d8ba5faad3c4c24b971cc0d7dbbf90 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Sep 2021 11:07:36 +0200 Subject: [PATCH 2/6] Room detail : lazy load EmojiPopup and AutocompleteMemberPresenter --- .../app/core/platform/LifecycleAwareLazy.kt | 75 +++++++++++++++++++ .../member/AutocompleteMemberPresenter.kt | 4 +- .../home/room/detail/RoomDetailFragment.kt | 26 ++++--- 3 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt diff --git a/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt new file mode 100644 index 0000000000..283106232e --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/platform/LifecycleAwareLazy.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2021 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.app.core.platform + +import androidx.annotation.MainThread +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent + +fun LifecycleOwner.lifecycleAwareLazy(initializer: () -> T): Lazy = LifecycleAwareLazy(this, initializer) + +private object UninitializedValue + +class LifecycleAwareLazy( + private val owner: LifecycleOwner, + initializer: () -> T +) : Lazy, LifecycleObserver { + + private var initializer: (() -> T)? = initializer + + private var _value: Any? = UninitializedValue + + @Suppress("UNCHECKED_CAST") + override val value: T + @MainThread + get() { + if (_value === UninitializedValue) { + _value = initializer!!() + attachToLifecycle() + } + return _value as T + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + fun resetValue() { + _value = UninitializedValue + detachFromLifecycle() + } + + private fun attachToLifecycle() { + if (getLifecycleOwner().lifecycle.currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("Initialization failed because lifecycle has been destroyed!") + } + getLifecycleOwner().lifecycle.addObserver(this) + } + + private fun detachFromLifecycle() { + getLifecycleOwner().lifecycle.removeObserver(this) + } + + private fun getLifecycleOwner() = when (owner) { + is Fragment -> owner.viewLifecycleOwner + else -> owner + } + + override fun isInitialized(): Boolean = _value !== UninitializedValue + + override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet." +} diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt index aa0c10e0a2..4976cb39b9 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/member/AutocompleteMemberPresenter.kt @@ -19,8 +19,8 @@ package im.vector.app.features.autocomplete.member import android.content.Context import androidx.recyclerview.widget.RecyclerView import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import org.matrix.android.sdk.api.query.QueryStringValue @@ -35,7 +35,7 @@ class AutocompleteMemberPresenter @AssistedInject constructor(context: Context, private val controller: AutocompleteMemberController ) : RecyclerViewPresenter(context), AutocompleteClickListener { - private val room = session.getRoom(roomId)!! + private val room by lazy { session.getRoom(roomId)!! } init { controller.listener = this diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index d19df302f0..1608ec12f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -87,6 +87,7 @@ import im.vector.app.core.hardware.vibrate import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.lifecycleAwareLazy import im.vector.app.core.platform.showOptimizedSnackbar import im.vector.app.core.resources.ColorProvider import im.vector.app.core.ui.views.CurrentCallsView @@ -314,7 +315,9 @@ class RoomDetailFragment @Inject constructor( private val currentCallsViewPresenter = CurrentCallsViewPresenter() private var inviteView: VectorInviteView? = null - private lateinit var emojiPopup: EmojiPopup + private val emojiPopup: EmojiPopup by lifecycleAwareLazy { + createEmojiPopup() + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -323,6 +326,9 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) } } + lifecycleScope.launchWhenResumed { + + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -349,7 +355,7 @@ class RoomDetailFragment @Inject constructor( setupJumpToReadMarkerView() setupActiveCallView() setupJumpToBottomView() - setupEmojiPopup() + setupEmojiButton() setupFailedMessagesWarningView() setupRemoveJitsiWidgetView() setupVoiceMessageView() @@ -585,8 +591,14 @@ class RoomDetailFragment @Inject constructor( ) } - private fun setupEmojiPopup() { - emojiPopup = EmojiPopup + private fun setupEmojiButton() { + views.composerLayout.views.composerEmojiButton.debouncedClicks { + emojiPopup.toggle() + } + } + + private fun createEmojiPopup(): EmojiPopup { + return EmojiPopup .Builder .fromRootView(views.rootConstraintLayout) .setKeyboardAnimationStyle(R.style.emoji_fade_animation_style) @@ -603,10 +615,6 @@ class RoomDetailFragment @Inject constructor( } } .build(views.composerLayout.views.composerEditText) - - views.composerLayout.views.composerEmojiButton.debouncedClicks { - emojiPopup.toggle() - } } private fun setupFailedMessagesWarningView() { @@ -775,8 +783,6 @@ class RoomDetailFragment @Inject constructor( autoCompleter.clear() debouncer.cancelAll() views.timelineRecyclerView.cleanup() - emojiPopup.dismiss() - super.onDestroyView() } From 290586948fa855888a67073e17b0daaa2e0f07c9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Sep 2021 11:36:37 +0200 Subject: [PATCH 3/6] Room detail: create lazy loaded views holder --- .../home/room/detail/RoomDetailFragment.kt | 16 +++---- .../detail/views/RoomDetailLazyLoadedViews.kt | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 1608ec12f9..1ee14638a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -76,7 +76,6 @@ import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.hideKeyboard -import im.vector.app.core.extensions.inflateIfNeeded import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showKeyboard @@ -155,6 +154,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet +import im.vector.app.features.home.room.detail.views.RoomDetailLazyLoadedViews import im.vector.app.features.home.room.detail.widget.RoomWidgetsBottomSheet import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillImageSpan @@ -314,7 +314,7 @@ class RoomDetailFragment @Inject constructor( private var lockSendButton = false private val currentCallsViewPresenter = CurrentCallsViewPresenter() - private var inviteView: VectorInviteView? = null + private val lazyLoadedViews = RoomDetailLazyLoadedViews() private val emojiPopup: EmojiPopup by lifecycleAwareLazy { createEmojiPopup() } @@ -326,9 +326,6 @@ class RoomDetailFragment @Inject constructor( roomDetailViewModel.handle(RoomDetailAction.RoomUpgradeSuccess(replacementRoomId)) } } - lifecycleScope.launchWhenResumed { - - } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -348,6 +345,7 @@ class RoomDetailFragment @Inject constructor( onTapToReturnToCall = ::onTapToReturnToCall ) keyboardStateUtils = KeyboardStateUtils(requireActivity()) + lazyLoadedViews.bind(views) setupToolbar(views.roomToolbar) setupRecyclerView() setupComposer() @@ -776,6 +774,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroyView() { + lazyLoadedViews.unBind() timelineEventController.callback = null timelineEventController.removeModelBuildListener(modelBuildListener) currentCallsViewPresenter.unBind() @@ -1368,7 +1367,7 @@ class RoomDetailFragment @Inject constructor( views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages timelineEventController.update(state) - inviteView?.isVisible = false + lazyLoadedViews.inviteView?.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { @@ -1389,12 +1388,9 @@ class RoomDetailFragment @Inject constructor( views.notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneEvent)) } } else if (summary?.membership == Membership.INVITE && inviter != null) { - views.inviteViewStub.inflateIfNeeded { - inviteView = it - } views.composerLayout.isVisible = false views.voiceMessageRecorderView.isVisible = false - inviteView?.apply { + lazyLoadedViews.inviteView?.apply { callback = this@RoomDetailFragment isVisible = true render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt new file mode 100644 index 0000000000..d194ecf5f1 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 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.app.features.home.room.detail.views + +import im.vector.app.core.extensions.inflateIfNeeded +import im.vector.app.databinding.FragmentRoomDetailBinding +import im.vector.app.features.invite.VectorInviteView + +/** + * This is an holder for lazy loading some views of the RoomDetail screen. + * It's using some ViewStub where it makes sense. + */ +class RoomDetailLazyLoadedViews { + + private var roomDetailBinding: FragmentRoomDetailBinding? = null + + var inviteView: VectorInviteView? = null + private set + get() { + roomDetailBinding?.inviteViewStub?.inflateIfNeeded { + inviteView = it + } + return field + } + + fun bind(roomDetailBinding: FragmentRoomDetailBinding) { + this.roomDetailBinding = roomDetailBinding + } + + fun unBind() { + roomDetailBinding = null + inviteView = null + } +} From ebd50956625b793842b02f240e4d67301eb8cebc Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 22 Sep 2021 18:00:56 +0200 Subject: [PATCH 4/6] Room detail: use ViewStub for FailedMessagesWarningView --- .../im/vector/app/core/extensions/ViewStub.kt | 26 -------------- .../ui/views/FailedMessagesWarningView.kt | 5 --- .../home/room/detail/RoomDetailFragment.kt | 31 +++++++++-------- .../detail/views/RoomDetailLazyLoadedViews.kt | 34 ++++++++++++++----- .../main/res/layout/fragment_room_detail.xml | 11 +++--- ...iew_stub_failed_message_warning_layout.xml | 5 +++ 6 files changed, 52 insertions(+), 60 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt create mode 100644 vector/src/main/res/layout/view_stub_failed_message_warning_layout.xml diff --git a/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt b/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt deleted file mode 100644 index 33b3f742cc..0000000000 --- a/vector/src/main/java/im/vector/app/core/extensions/ViewStub.kt +++ /dev/null @@ -1,26 +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.app.core.extensions - -import android.view.View -import android.view.ViewStub - -inline fun ViewStub.inflateIfNeeded(onInflate: (T) -> Unit) { - if (parent != null) { - onInflate(inflate() as T) - } -} diff --git a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt index f9518552a3..755230b5bf 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/FailedMessagesWarningView.kt @@ -19,7 +19,6 @@ package im.vector.app.core.ui.views import android.content.Context import android.util.AttributeSet import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewFailedMessagesWarningBinding @@ -49,8 +48,4 @@ class FailedMessagesWarningView @JvmOverloads constructor( views.failedMessagesDeleteAllButton.setOnClickListener { callback?.onDeleteAllClicked() } views.failedMessagesRetryButton.setOnClickListener { callback?.onRetryClicked() } } - - fun render(hasFailedMessages: Boolean) { - isVisible = hasFailedMessages - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 1ee14638a5..0e7551087b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -354,7 +354,6 @@ class RoomDetailFragment @Inject constructor( setupActiveCallView() setupJumpToBottomView() setupEmojiButton() - setupFailedMessagesWarningView() setupRemoveJitsiWidgetView() setupVoiceMessageView() @@ -615,8 +614,16 @@ class RoomDetailFragment @Inject constructor( .build(views.composerLayout.views.composerEditText) } - private fun setupFailedMessagesWarningView() { - views.failedMessagesWarningView.callback = object : FailedMessagesWarningView.Callback { + private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + // In this case, let the user start again the gesture + } else if (deniedPermanently) { + vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) + } + } + + private fun createFailedMessagesWarningCallback(): FailedMessagesWarningView.Callback { + return object : FailedMessagesWarningView.Callback { override fun onDeleteAllClicked() { MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.event_status_delete_all_failed_dialog_title) @@ -634,14 +641,6 @@ class RoomDetailFragment @Inject constructor( } } - private val permissionVoiceMessageLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> - if (allGranted) { - // In this case, let the user start again the gesture - } else if (deniedPermanently) { - vectorBaseActivity.onPermissionDeniedSnackbar(R.string.denied_permission_voice_message) - } - } - private fun setupVoiceMessageView() { views.voiceMessageRecorderView.voiceMessagePlaybackTracker = voiceMessagePlaybackTracker @@ -1361,13 +1360,17 @@ class RoomDetailFragment @Inject constructor( val summary = state.asyncRoomSummary() renderToolbar(summary, state.typingMessage) views.removeJitsiWidgetView.render(state) - views.failedMessagesWarningView.render(state.hasFailedSending) + if (state.hasFailedSending) { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = true, createFailedMessagesWarningCallback())?.isVisible = true + } else { + lazyLoadedViews.failedMessagesWarningView(inflateIfNeeded = false)?.isVisible = false + } val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages timelineEventController.update(state) - lazyLoadedViews.inviteView?.isVisible = false + lazyLoadedViews.inviteView(false)?.isVisible = false if (state.tombstoneEvent == null) { if (state.canSendMessage) { if (!views.voiceMessageRecorderView.isActive()) { @@ -1390,7 +1393,7 @@ class RoomDetailFragment @Inject constructor( } else if (summary?.membership == Membership.INVITE && inviter != null) { views.composerLayout.isVisible = false views.voiceMessageRecorderView.isVisible = false - lazyLoadedViews.inviteView?.apply { + lazyLoadedViews.inviteView(true)?.apply { callback = this@RoomDetailFragment isVisible = true render(inviter, VectorInviteView.Mode.LARGE, state.changeMembershipState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt index d194ecf5f1..708f23aba1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt @@ -16,9 +16,13 @@ package im.vector.app.features.home.room.detail.views -import im.vector.app.core.extensions.inflateIfNeeded +import android.view.View +import android.view.ViewStub +import im.vector.app.core.ui.views.FailedMessagesWarningView import im.vector.app.databinding.FragmentRoomDetailBinding +import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView import im.vector.app.features.invite.VectorInviteView +import kotlin.reflect.KMutableProperty0 /** * This is an holder for lazy loading some views of the RoomDetail screen. @@ -28,14 +32,8 @@ class RoomDetailLazyLoadedViews { private var roomDetailBinding: FragmentRoomDetailBinding? = null - var inviteView: VectorInviteView? = null - private set - get() { - roomDetailBinding?.inviteViewStub?.inflateIfNeeded { - inviteView = it - } - return field - } + private var failedMessagesWarningView: FailedMessagesWarningView? = null + private var inviteView: VectorInviteView? = null fun bind(roomDetailBinding: FragmentRoomDetailBinding) { this.roomDetailBinding = roomDetailBinding @@ -44,5 +42,23 @@ class RoomDetailLazyLoadedViews { fun unBind() { roomDetailBinding = null inviteView = null + failedMessagesWarningView = null + } + + fun failedMessagesWarningView(inflateIfNeeded: Boolean, callback: FailedMessagesWarningView.Callback? = null): FailedMessagesWarningView? { + return getOrInflate(inflateIfNeeded, roomDetailBinding?.failedMessagesWarningStub, this::failedMessagesWarningView)?.apply { + this.callback = callback + } + } + + fun inviteView(inflateIfNeeded: Boolean): VectorInviteView? { + return getOrInflate(inflateIfNeeded, roomDetailBinding?.inviteViewStub, this::inviteView) + } + + private inline fun getOrInflate(inflateIfNeeded: Boolean, stub: ViewStub?, reference: KMutableProperty0): T? { + if (!inflateIfNeeded || stub == null || stub.parent == null) return reference.get() + val inflatedView = stub.inflate() as T + reference.set(inflatedView) + return inflatedView } } diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 482cdbc700..66dbbd2840 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -155,15 +155,14 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - + app:layout_constraintStart_toStartOf="parent" /> + app:constraint_referenced_ids="composerLayout,notificationAreaView, failedMessagesWarningStub" /> + \ No newline at end of file From fc5c6b9b007468e6ba8c933f2b39ebafb58f1647 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Sep 2021 13:13:16 +0200 Subject: [PATCH 5/6] RoomDetail: lazy load EmojiDataSource data (+ async) --- .../reactions/data/EmojiDataSourceTest.kt | 68 +++++++++----- .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../im/vector/app/core/di/VectorComponent.kt | 3 + .../im/vector/app/core/di/VectorModule.kt | 14 +++ .../emoji/AutocompleteEmojiPresenter.kt | 22 +++-- .../reactions/EmojiChooserFragment.kt | 6 +- .../reactions/EmojiChooserViewModel.kt | 18 +++- .../reactions/EmojiReactionPickerActivity.kt | 26 +++--- .../reactions/EmojiRecyclerAdapter.kt | 34 ++++--- .../reactions/EmojiSearchResultViewModel.kt | 17 ++-- .../reactions/data/EmojiDataSource.kt | 89 ++++++++++--------- 11 files changed, 194 insertions(+), 105 deletions(-) diff --git a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt index 79090c42dd..a880b17e0c 100644 --- a/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt +++ b/vector/src/androidTest/java/im/vector/app/features/reactions/data/EmojiDataSourceTest.kt @@ -17,6 +17,10 @@ package im.vector.app.features.reactions.data import im.vector.app.InstrumentedTest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.FixMethodOrder @@ -30,64 +34,80 @@ import kotlin.system.measureTimeMillis @FixMethodOrder(MethodSorters.JVM) class EmojiDataSourceTest : InstrumentedTest { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + @Test fun checkParsingTime() { val time = measureTimeMillis { - EmojiDataSource(context().resources) + createEmojiDataSource() } - assertTrue("Too long to parse", time < 100) } @Test fun checkNumberOfResult() { - val emojiDataSource = EmojiDataSource(context().resources) - assertTrue("Wrong number of emojis", emojiDataSource.rawData.emojis.size >= 500) - assertTrue("Wrong number of categories", emojiDataSource.rawData.categories.size >= 8) + val emojiDataSource = createEmojiDataSource() + val rawData = runBlocking { + emojiDataSource.rawData.await() + } + assertTrue("Wrong number of emojis", rawData.emojis.size >= 500) + assertTrue("Wrong number of categories", rawData.categories.size >= 8) } @Test fun searchTestEmptySearch() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Empty search should return at least 500 results", emojiDataSource.filterWith("").size >= 500) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("") + } + assertTrue("Empty search should return at least 500 results", result.size >= 500) } @Test fun searchTestNoResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should not have result", emojiDataSource.filterWith("noresult").isEmpty()) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("noresult") + } + assertTrue("Should not have result", result.isEmpty()) } @Test fun searchTestOneResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 1 result", 1, emojiDataSource.filterWith("france").size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("france") + } + assertEquals("Should have 1 result", 1, result.size) } @Test fun searchTestManyResult() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertTrue("Should have many result", emojiDataSource.filterWith("fra").size > 1) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("fra") + } + assertTrue("Should have many result", result.size > 1) } @Test fun testTada() { - val emojiDataSource = EmojiDataSource(context().resources) - - val result = emojiDataSource.filterWith("tada") - + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.filterWith("tada") + } assertEquals("Should find tada emoji", 1, result.size) assertEquals("Should find tada emoji", "🎉", result[0].emoji) } @Test fun testQuickReactions() { - val emojiDataSource = EmojiDataSource(context().resources) - - assertEquals("Should have 8 quick reactions", 8, emojiDataSource.getQuickReactions().size) + val emojiDataSource = createEmojiDataSource() + val result = runBlocking { + emojiDataSource.getQuickReactions() + } + assertEquals("Should have 8 quick reactions", 8, result.size) } + + private fun createEmojiDataSource() = EmojiDataSource(coroutineScope, context().resources) } diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 40ba57103b..a02b5256a5 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -98,6 +98,7 @@ import im.vector.app.features.usercode.UserCodeActivity import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.workers.signout.SignOutBottomSheetDialogFragment +import kotlinx.coroutines.CoroutineScope @Component( dependencies = [ @@ -129,6 +130,7 @@ interface ScreenComponent { fun uiStateRepository(): UiStateRepository fun unrecognizedCertificateDialog(): UnrecognizedCertificateDialog fun autoAcceptInvites(): AutoAcceptInvites + fun appCoroutineScope(): CoroutineScope /* ========================================================================================== * Activities diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 68b212c830..54f07a7b0e 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -60,6 +60,7 @@ import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.session.SessionListener import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService @@ -165,6 +166,8 @@ interface VectorComponent { fun webRtcCallManager(): WebRtcCallManager + fun appCoroutineScope(): CoroutineScope + fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder @Component.Factory diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 006a2f5aa0..4d863f04d3 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -33,12 +33,18 @@ import im.vector.app.features.pin.PinCodeStore import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.HomeServerHistoryService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.session.Session +import javax.inject.Singleton @Module abstract class VectorModule { @@ -94,6 +100,14 @@ abstract class VectorModule { fun providesHomeServerHistoryService(matrix: Matrix): HomeServerHistoryService { return matrix.homeServerHistoryService() } + + @Provides + @JvmStatic + @Singleton + fun providesApplicationCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.Main) + } + } @Binds diff --git a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt index bf180746de..4f272c7a24 100644 --- a/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt +++ b/vector/src/main/java/im/vector/app/features/autocomplete/emoji/AutocompleteEmojiPresenter.kt @@ -21,6 +21,11 @@ import androidx.recyclerview.widget.RecyclerView import im.vector.app.features.autocomplete.AutocompleteClickListener import im.vector.app.features.autocomplete.RecyclerViewPresenter import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import javax.inject.Inject class AutocompleteEmojiPresenter @Inject constructor(context: Context, @@ -28,11 +33,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, private val controller: AutocompleteEmojiController) : RecyclerViewPresenter(context), AutocompleteClickListener { + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + init { controller.listener = this } fun clear() { + coroutineScope.coroutineContext.cancelChildren() controller.listener = null } @@ -45,12 +53,14 @@ class AutocompleteEmojiPresenter @Inject constructor(context: Context, } override fun onQuery(query: CharSequence?) { - val data = if (query.isNullOrBlank()) { - // Return common emojis - emojiDataSource.getQuickReactions() - } else { - emojiDataSource.filterWith(query.toString()) + coroutineScope.launch { + val data = if (query.isNullOrBlank()) { + // Return common emojis + emojiDataSource.getQuickReactions() + } else { + emojiDataSource.filterWith(query.toString()) + } + controller.setData(data) } - controller.setData(data) } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt index 51dc62af8b..822f291e1f 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserFragment.kt @@ -41,15 +41,15 @@ class EmojiChooserFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel = activityViewModelProvider.get(EmojiChooserViewModel::class.java) - emojiRecyclerAdapter.reactionClickListener = this emojiRecyclerAdapter.interactionListener = this - views.emojiRecyclerView.adapter = emojiRecyclerAdapter - viewModel.moveToSection.observe(viewLifecycleOwner) { section -> emojiRecyclerAdapter.scrollToSection(section) } + viewModel.emojiData.observe(viewLifecycleOwner) { + emojiRecyclerAdapter.update(it) + } } override fun getCoroutineScope() = lifecycleScope diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt index df2085e41b..3a4caa296a 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiChooserViewModel.kt @@ -17,11 +17,16 @@ package im.vector.app.features.reactions import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import im.vector.app.core.utils.LiveEvent +import im.vector.app.features.reactions.data.EmojiData +import im.vector.app.features.reactions.data.EmojiDataSource +import kotlinx.coroutines.launch import javax.inject.Inject -class EmojiChooserViewModel @Inject constructor() : ViewModel() { +class EmojiChooserViewModel @Inject constructor(private val emojiDataSource: EmojiDataSource) : ViewModel() { + val emojiData: MutableLiveData = MutableLiveData() val navigateEvent: MutableLiveData> = MutableLiveData() var selectedReaction: String? = null var eventId: String? = null @@ -29,6 +34,17 @@ class EmojiChooserViewModel @Inject constructor() : ViewModel() { val currentSection: MutableLiveData = MutableLiveData() val moveToSection: MutableLiveData = MutableLiveData() + init { + loadEmojiData() + } + + private fun loadEmojiData() { + viewModelScope.launch { + val rawData = emojiDataSource.rawData.await() + emojiData.postValue(rawData) + } + } + fun onReactionSelected(reaction: String) { selectedReaction = reaction navigateEvent.value = LiveEvent(NAVIGATE_FINISH) diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt index ecfaf93747..7140bb0baa 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiReactionPickerActivity.kt @@ -25,6 +25,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.widget.SearchView import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.viewModel import com.google.android.material.tabs.TabLayout import com.jakewharton.rxbinding3.widget.queryTextChanges @@ -36,6 +37,7 @@ import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivityEmojiReactionPickerBinding import im.vector.app.features.reactions.data.EmojiDataSource import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit @@ -91,17 +93,19 @@ class EmojiReactionPickerActivity : VectorBaseActivity - val s = category.emojis[0] - views.tabs.newTab() - .also { tab -> - tab.text = emojiDataSource.rawData.emojis[s]!!.emoji - tab.contentDescription = category.name - } - .also { tab -> - views.tabs.addTab(tab) - } + lifecycleScope.launch { + val rawData = emojiDataSource.rawData.await() + rawData.categories.forEach { category -> + val s = category.emojis[0] + views.tabs.newTab() + .also { tab -> + tab.text = rawData.emojis[s]!!.emoji + tab.contentDescription = category.name + } + .also { tab -> + views.tabs.addTab(tab) + } + } } views.tabs.addOnTabSelectedListener(tabLayoutSelectionListener) diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 45d26e81eb..5fbd02e75f 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -15,6 +15,7 @@ */ package im.vector.app.features.reactions +import android.annotation.SuppressLint import android.os.Build import android.os.Trace import android.text.Layout @@ -30,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R +import im.vector.app.features.reactions.data.EmojiData import im.vector.app.features.reactions.data.EmojiDataSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -43,13 +45,13 @@ import kotlin.math.abs * TODO: Performances * TODO: Scroll to section - Find a way to snap section to the top */ -class EmojiRecyclerAdapter @Inject constructor( - private val dataSource: EmojiDataSource -) : +class EmojiRecyclerAdapter @Inject constructor() : RecyclerView.Adapter() { var reactionClickListener: ReactionClickListener? = null var interactionListener: InteractionListener? = null + + private var rawData: EmojiData = EmojiData(emptyList(), emptyMap(), emptyMap()) private var mRecyclerView: RecyclerView? = null private var currentFirstVisibleSection = 0 @@ -61,6 +63,12 @@ class EmojiRecyclerAdapter @Inject constructor( UNKNOWN } + @SuppressLint("NotifyDataSetChanged") + fun update(emojiData: EmojiData){ + rawData = emojiData + notifyDataSetChanged() + } + private var scrollState = ScrollState.UNKNOWN private var isFastScroll = false @@ -71,10 +79,10 @@ class EmojiRecyclerAdapter @Inject constructor( if (itemPosition != RecyclerView.NO_POSITION) { val sectionNumber = getSectionForAbsoluteIndex(itemPosition) if (!isSection(itemPosition)) { - val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis + val sectionMojis = rawData.categories[sectionNumber].emojis val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[itemPosition - sectionOffset] - val item = dataSource.rawData.emojis.getValue(emoji).emoji + val item = rawData.emojis.getValue(emoji).emoji reactionClickListener?.onReactionSelected(item) } } @@ -115,7 +123,7 @@ class EmojiRecyclerAdapter @Inject constructor( } fun scrollToSection(section: Int) { - if (section < 0 || section >= dataSource.rawData.categories.size) { + if (section < 0 || section >= rawData.categories.size) { // ignore return } @@ -149,7 +157,7 @@ class EmojiRecyclerAdapter @Inject constructor( private fun isSection(position: Int): Boolean { var sectionOffset = 1 var lastItemInSection: Int - dataSource.rawData.categories.forEach { category -> + rawData.categories.forEach { category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (position == sectionOffset - 1) return true sectionOffset = lastItemInSection + 2 @@ -161,7 +169,7 @@ class EmojiRecyclerAdapter @Inject constructor( var sectionOffset = 1 var lastItemInSection: Int var index = 0 - dataSource.rawData.categories.forEach { category -> + rawData.categories.forEach { category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (position <= lastItemInSection) return index sectionOffset = lastItemInSection + 2 @@ -174,7 +182,7 @@ class EmojiRecyclerAdapter @Inject constructor( // Todo cache this for fast access var sectionOffset = 1 var lastItemInSection: Int - dataSource.rawData.categories.forEachIndexed { index, category -> + rawData.categories.forEachIndexed { index, category -> lastItemInSection = sectionOffset + category.emojis.size - 1 if (section == index) return sectionOffset sectionOffset = lastItemInSection + 2 @@ -186,12 +194,12 @@ class EmojiRecyclerAdapter @Inject constructor( Trace.beginSection("MyAdapter.onBindViewHolder") val sectionNumber = getSectionForAbsoluteIndex(position) if (isSection(position)) { - holder.bind(dataSource.rawData.categories[sectionNumber].name) + holder.bind(rawData.categories[sectionNumber].name) } else { - val sectionMojis = dataSource.rawData.categories[sectionNumber].emojis + val sectionMojis = rawData.categories[sectionNumber].emojis val sectionOffset = getSectionOffset(sectionNumber) val emoji = sectionMojis[position - sectionOffset] - val item = dataSource.rawData.emojis[emoji]!!.emoji + val item = rawData.emojis[emoji]!!.emoji (holder as EmojiViewHolder).data = item if (scrollState != ScrollState.SETTLING || !isFastScroll) { // Log.i("PERF","Bind with draw at position:$position") @@ -220,7 +228,7 @@ class EmojiRecyclerAdapter @Inject constructor( super.onViewRecycled(holder) } - override fun getItemCount() = dataSource.rawData.categories + override fun getItemCount() = rawData.categories .sumOf { emojiCategory -> 1 /* Section */ + emojiCategory.emojis.size } abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt index ac7aee797a..8e12dd2cca 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiSearchResultViewModel.kt @@ -15,17 +15,19 @@ */ package im.vector.app.features.reactions +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.features.reactions.data.EmojiItem +import kotlinx.coroutines.launch data class EmojiSearchResultViewState( val query: String = "", @@ -58,11 +60,14 @@ class EmojiSearchResultViewModel @AssistedInject constructor( } private fun updateQuery(action: EmojiSearchAction.UpdateQuery) { - setState { - copy( - query = action.queryString, - results = dataSource.filterWith(action.queryString) - ) + viewModelScope.launch { + val results = dataSource.filterWith(action.queryString) + setState { + copy( + query = action.queryString, + results = results + ) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt index 96eda22eb9..7218eb993b 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/data/EmojiDataSource.kt @@ -20,53 +20,60 @@ import android.graphics.Paint import androidx.core.graphics.PaintCompat import com.squareup.moshi.Moshi import im.vector.app.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import javax.inject.Inject import javax.inject.Singleton @Singleton class EmojiDataSource @Inject constructor( + appScope: CoroutineScope, resources: Resources ) { private val paint = Paint() - val rawData = resources.openRawResource(R.raw.emoji_picker_datasource) - .use { input -> - Moshi.Builder() - .build() - .adapter(EmojiData::class.java) - .fromJson(input.bufferedReader().use { it.readText() }) - } - ?.let { parsedRawData -> - // Add key as a keyword, it will solve the issue that ":tada" is not available in completion - // Only add emojis to emojis/categories that can be rendered by the system - parsedRawData.copy( - emojis = mutableMapOf().apply { - parsedRawData.emojis.keys.forEach { key -> - val origin = parsedRawData.emojis[key] ?: return@forEach + val rawData = appScope.async(Dispatchers.IO, CoroutineStart.LAZY) { + resources.openRawResource(R.raw.emoji_picker_datasource) + .use { input -> + Moshi.Builder() + .build() + .adapter(EmojiData::class.java) + .fromJson(input.bufferedReader().use { it.readText() }) + } + ?.let { parsedRawData -> + // Add key as a keyword, it will solve the issue that ":tada" is not available in completion + // Only add emojis to emojis/categories that can be rendered by the system + parsedRawData.copy( + emojis = mutableMapOf().apply { + parsedRawData.emojis.keys.forEach { key -> + val origin = parsedRawData.emojis[key] ?: return@forEach - // Do not add keys containing '_' - if (isEmojiRenderable(origin.emoji)) { - if (origin.keywords.contains(key) || key.contains("_")) { - put(key, origin) - } else { - put(key, origin.copy(keywords = origin.keywords + key)) - } - } - } - }, - categories = mutableListOf().apply { - parsedRawData.categories.forEach { entry -> - add(EmojiCategory(entry.id, entry.name, mutableListOf().apply { - entry.emojis.forEach { e -> - if (isEmojiRenderable(parsedRawData.emojis[e]!!.emoji)) { - add(e) + // Do not add keys containing '_' + if (isEmojiRenderable(origin.emoji)) { + if (origin.keywords.contains(key) || key.contains("_")) { + put(key, origin) + } else { + put(key, origin.copy(keywords = origin.keywords + key)) } } - })) + } + }, + categories = mutableListOf().apply { + parsedRawData.categories.forEach { entry -> + add(EmojiCategory(entry.id, entry.name, mutableListOf().apply { + entry.emojis.forEach { e -> + if (isEmojiRenderable(parsedRawData.emojis[e]!!.emoji)) { + add(e) + } + } + })) + } } - } - ) - } - ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + ) + } + ?: EmojiData(emptyList(), emptyMap(), emptyMap()) + } private val quickReactions = mutableListOf() @@ -74,9 +81,9 @@ class EmojiDataSource @Inject constructor( return PaintCompat.hasGlyph(paint, emoji) } - fun filterWith(query: String): List { + suspend fun filterWith(query: String): List { val words = query.split("\\s".toRegex()) - + val rawData = this.rawData.await() // First add emojis with name matching query, sorted by name return (rawData.emojis.values .asSequence() @@ -87,9 +94,9 @@ class EmojiDataSource @Inject constructor( // Then emojis with keyword matching any of the word in the query, sorted by name rawData.emojis.values .filter { emojiItem -> - words.fold(true, { prev, word -> + words.fold(true) { prev, word -> prev && emojiItem.keywords.any { keyword -> keyword.contains(word, true) } - }) + } } .sortedBy { it.name }) // and ensure they will not be present twice @@ -97,7 +104,7 @@ class EmojiDataSource @Inject constructor( .toList() } - fun getQuickReactions(): List { + suspend fun getQuickReactions(): List { if (quickReactions.isEmpty()) { listOf( "thumbs-up", // 👍 @@ -109,7 +116,7 @@ class EmojiDataSource @Inject constructor( "rocket", // 🚀 "eyes" // 👀 ) - .mapNotNullTo(quickReactions) { rawData.emojis[it] } + .mapNotNullTo(quickReactions) { rawData.await().emojis[it] } } return quickReactions From d9e9568c2e9fbd855c3de266f6322660f265c714 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 23 Sep 2021 13:19:29 +0200 Subject: [PATCH 6/6] Clean code and update CHANGES --- changelog.d/4065.misc | 1 + vector/src/main/java/im/vector/app/core/di/VectorModule.kt | 3 --- .../home/room/detail/views/RoomDetailLazyLoadedViews.kt | 1 - .../im/vector/app/features/reactions/EmojiRecyclerAdapter.kt | 3 +-- 4 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 changelog.d/4065.misc diff --git a/changelog.d/4065.misc b/changelog.d/4065.misc new file mode 100644 index 0000000000..35725f7fa1 --- /dev/null +++ b/changelog.d/4065.misc @@ -0,0 +1 @@ +Improve performances on RoomDetail screen \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt index 4d863f04d3..dd1ffee8ec 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorModule.kt @@ -34,9 +34,7 @@ import im.vector.app.features.pin.SharedPrefPinCodeStore import im.vector.app.features.ui.SharedPreferencesUiStateRepository import im.vector.app.features.ui.UiStateRepository import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.SupervisorJob import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.auth.AuthenticationService @@ -107,7 +105,6 @@ abstract class VectorModule { fun providesApplicationCoroutineScope(): CoroutineScope { return CoroutineScope(SupervisorJob() + Dispatchers.Main) } - } @Binds diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt index 708f23aba1..fafb49ad5c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/views/RoomDetailLazyLoadedViews.kt @@ -20,7 +20,6 @@ import android.view.View import android.view.ViewStub import im.vector.app.core.ui.views.FailedMessagesWarningView import im.vector.app.databinding.FragmentRoomDetailBinding -import im.vector.app.features.home.room.detail.composer.VoiceMessageRecorderView import im.vector.app.features.invite.VectorInviteView import kotlin.reflect.KMutableProperty0 diff --git a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt index 5fbd02e75f..d64ee0f705 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/EmojiRecyclerAdapter.kt @@ -32,7 +32,6 @@ import androidx.transition.AutoTransition import androidx.transition.TransitionManager import im.vector.app.R import im.vector.app.features.reactions.data.EmojiData -import im.vector.app.features.reactions.data.EmojiDataSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -64,7 +63,7 @@ class EmojiRecyclerAdapter @Inject constructor() : } @SuppressLint("NotifyDataSetChanged") - fun update(emojiData: EmojiData){ + fun update(emojiData: EmojiData) { rawData = emojiData notifyDataSetChanged() }