From a4e8f0c8fd0a5963487e0214e6b9fdf5624b3679 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 30 Aug 2023 12:47:31 +0100 Subject: [PATCH] Fix tests and improve structure of CustomReactionState - Fix tests - Improve structure of CustomReactionState --- .../io/element/android/x/di/AppModule.kt | 8 +++++ .../messages/impl/MessagesStateProvider.kt | 3 +- .../features/messages/impl/MessagesView.kt | 4 +-- .../CustomReactionBottomSheet.kt | 14 ++++---- .../customreaction/CustomReactionPresenter.kt | 34 +++++++++++-------- .../customreaction/CustomReactionState.kt | 20 +++++++++-- .../DefaultEmojibaseProvider.kt | 27 +++++++++++++++ .../components/customreaction/EmojiPicker.kt | 6 ++-- .../customreaction/EmojibaseProvider.kt | 23 +++++++++++++ .../messages/MessagesPresenterTest.kt | 3 +- .../CustomReactionPresenterTests.kt | 33 ++++++++++++------ .../customreaction/FakeEmojibaseProvider.kt | 26 ++++++++++++++ 12 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/DefaultEmojibaseProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseProvider.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/FakeEmojibaseProvider.kt diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index a1d0b50522..f8ab5532b0 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -23,6 +23,8 @@ import androidx.preference.PreferenceManager import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides +import io.element.android.features.messages.impl.timeline.components.customreaction.DefaultEmojibaseProvider +import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType @@ -105,4 +107,10 @@ object AppModule { fun provideSnackbarDispatcher(): SnackbarDispatcher { return SnackbarDispatcher() } + + @Provides + @SingleIn(AppScope::class) + fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider { + return DefaultEmojibaseProvider(context) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index ecc9b1746a..7eb1a0984e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -67,8 +67,7 @@ fun aMessagesState() = MessagesState( ), actionListState = anActionListState(), customReactionState = CustomReactionState( - selectedEventId = null, - emojiProvider = Async.Uninitialized, + target = CustomReactionState.Target.None, eventSink = {}, selectedEmoji = persistentSetOf(), ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 5608711171..74dcb610af 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -200,11 +200,9 @@ fun MessagesView( CustomReactionBottomSheet( state = state.customReactionState, - onEmojiSelected = { emoji -> - state.customReactionState.selectedEventId?.let { eventId -> + onEmojiSelected = { eventId, emoji -> state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) state.customReactionState.eventSink(CustomReactionEvents.DismissCustomReactionSheet) - } } ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt index 05c2956316..ed78993716 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -23,33 +23,35 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import io.element.android.emojibasebindings.Emoji +import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.hide +import io.element.android.libraries.matrix.api.core.EventId @OptIn(ExperimentalMaterial3Api::class) @Composable fun CustomReactionBottomSheet( state: CustomReactionState, - onEmojiSelected: (Emoji) -> Unit, + onEmojiSelected: (EventId, Emoji) -> Unit, modifier: Modifier = Modifier, ) { val sheetState = rememberModalBottomSheetState() val coroutineScope = rememberCoroutineScope() + val target = state.target as? CustomReactionState.Target.Success fun onDismiss() { state.eventSink(CustomReactionEvents.DismissCustomReactionSheet) } fun onEmojiSelectedDismiss(emoji: Emoji) { + if (target?.event?.eventId == null) return sheetState.hide(coroutineScope) { state.eventSink(CustomReactionEvents.DismissCustomReactionSheet) - onEmojiSelected(emoji) + onEmojiSelected(target.event.eventId, emoji) } } - val emojiProvider = state.emojiProvider.dataOrNull() - val selectedEventId = state.selectedEventId - if (emojiProvider != null && selectedEventId != null) { + if (target?.emojibaseStore != null && target.event.eventId != null) { ModalBottomSheet( onDismissRequest = ::onDismiss, sheetState = sheetState, @@ -57,7 +59,7 @@ fun CustomReactionBottomSheet( ) { EmojiPicker( onEmojiSelected = ::onEmojiSelectedDismiss, - emojiProvider = emojiProvider, + emojibaseStore = target.emojibaseStore, selectedEmojis = state.selectedEmoji, modifier = Modifier.fillMaxSize(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt index 4ab94f1656..8a331438a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt @@ -17,42 +17,46 @@ package io.element.android.features.messages.impl.timeline.components.customreaction import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.launch import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.canReact import kotlinx.collections.immutable.toImmutableSet import javax.inject.Inject -class CustomReactionPresenter @Inject constructor() : Presenter { +class CustomReactionPresenter @Inject constructor( + private val emojibaseProvider: EmojibaseProvider +) : Presenter { @Composable override fun present(): CustomReactionState { - var selectedEvent by remember { mutableStateOf(null) } - var emojiState: Async by remember { - mutableStateOf(Async.Uninitialized) + val target: MutableState = remember { + mutableStateOf(CustomReactionState.Target.None) } + val localCoroutineScope = rememberCoroutineScope() - val context = LocalContext.current fun handleShowCustomReactionSheet(event: TimelineItem.Event) { - selectedEvent = event - emojiState = Async.Loading() + target.value = CustomReactionState.Target.Loading(event) localCoroutineScope.launch { - emojiState = Async.Success(EmojibaseDatasource().load(context)) + target.value = CustomReactionState.Target.Success( + event = event, + emojibaseStore = emojibaseProvider.loadEmojibase() + ) } } fun handleDismissCustomReactionSheet() { - selectedEvent = null - emojiState = Async.Uninitialized + target.value = CustomReactionState.Target.None } fun handleEvents(event: CustomReactionEvents) { @@ -61,10 +65,10 @@ class CustomReactionPresenter @Inject constructor() : Presenter handleDismissCustomReactionSheet() } } - val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet() + val event = (target.value as? CustomReactionState.Target.Success)?.event + val selectedEmoji = event?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet() return CustomReactionState( - selectedEventId = selectedEvent?.eventId, - emojiProvider = emojiState, + target = target.value, selectedEmoji = selectedEmoji, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt index 09d5392d32..400c34d6fa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt @@ -17,13 +17,27 @@ package io.element.android.features.messages.impl.timeline.components.customreaction import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.features.messages.impl.actionlist.ActionListState +import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet data class CustomReactionState( - val selectedEventId: EventId?, - val emojiProvider: Async, + val target: Target, val selectedEmoji: ImmutableSet, val eventSink: (CustomReactionEvents) -> Unit, -) +) { + sealed interface Target { + + data object None : Target + data class Loading(val event: TimelineItem.Event) : Target + data class Success( + val event: TimelineItem.Event, + val emojibaseStore: EmojibaseStore, + ) : Target + } +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/DefaultEmojibaseProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/DefaultEmojibaseProvider.kt new file mode 100644 index 0000000000..2e66ea381e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/DefaultEmojibaseProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.messages.impl.timeline.components.customreaction + +import android.content.Context +import io.element.android.emojibasebindings.EmojibaseDatasource +import io.element.android.emojibasebindings.EmojibaseStore + +class DefaultEmojibaseProvider(val context: Context) :EmojibaseProvider { + override fun loadEmojibase(): EmojibaseStore { + return EmojibaseDatasource().load(context) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt index edbd6aabc6..1012d61020 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt @@ -63,12 +63,12 @@ import kotlinx.coroutines.launch @Composable fun EmojiPicker( onEmojiSelected: (Emoji) -> Unit, - emojiProvider: EmojibaseStore, + emojibaseStore: EmojibaseStore, selectedEmojis: ImmutableSet, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - val categories = remember { emojiProvider.categories } + val categories = remember { emojibaseStore.categories } val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.values().size }) Column(modifier) { TabRow( @@ -149,7 +149,7 @@ internal fun EmojiPickerDarkPreview() { private fun ContentToPreview() { EmojiPicker( onEmojiSelected = {}, - emojiProvider = EmojibaseDatasource().load(LocalContext.current), + emojibaseStore = EmojibaseDatasource().load(LocalContext.current), selectedEmojis = persistentSetOf("😀", "😄", "😃"), modifier = Modifier.fillMaxWidth(), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseProvider.kt new file mode 100644 index 0000000000..39739538d2 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.messages.impl.timeline.components.customreaction + +import io.element.android.emojibasebindings.EmojibaseStore + +interface EmojibaseProvider { + fun loadEmojibase(): EmojibaseStore +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 2c542e0054..c9f9bec3f8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.media.FakeLocalMediaFactory +import io.element.android.features.messages.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper @@ -584,7 +585,7 @@ class MessagesPresenterTest { ) val buildMeta = aBuildMeta() val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) - val customReactionPresenter = CustomReactionPresenter() + val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) return MessagesPresenter( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt index 9918a67296..bb77605132 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt @@ -24,27 +24,34 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.libraries.matrix.test.AN_EVENT_ID import kotlinx.coroutines.test.runTest import org.junit.Test class CustomReactionPresenterTests { - private val presenter = CustomReactionPresenter() + private val presenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) @Test fun `present - handle selecting and de-selecting an event`() = runTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.selectedEventId).isNull() - initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(aTimelineItemEvent(eventId = AN_EVENT_ID))) - assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID) + val event = aTimelineItemEvent(eventId = AN_EVENT_ID) + val initialState = awaitItem() + assertThat(initialState.target).isEqualTo(CustomReactionState.Target.None) + + initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event)) + + assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event)) + + val eventId = (awaitItem().target as? CustomReactionState.Target.Success)?.event?.eventId + assertThat(eventId).isEqualTo(AN_EVENT_ID) initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet) - assertThat(awaitItem().selectedEventId).isNull() + assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.None) } } @@ -53,13 +60,19 @@ class CustomReactionPresenterTests { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - val initialState = awaitItem() - assertThat(initialState.selectedEventId).isNull() val reactions = aTimelineItemReactions(count = 1, isHighlighted = true) + val event = aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions) + val initialState = awaitItem() + assertThat(initialState.target).isEqualTo(CustomReactionState.Target.None) + val key = reactions.reactions.first().key - initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions))) + initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event)) + + assertThat(awaitItem().target).isEqualTo(CustomReactionState.Target.Loading(event)) + val stateWithSelectedEmojis = awaitItem() - assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID) + val eventId = (stateWithSelectedEmojis.target as? CustomReactionState.Target.Success)?.event?.eventId + assertThat(eventId).isEqualTo(AN_EVENT_ID) assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/FakeEmojibaseProvider.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/FakeEmojibaseProvider.kt new file mode 100644 index 0000000000..434212030a --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/FakeEmojibaseProvider.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 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 io.element.android.features.messages.timeline.components.customreaction + +import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.features.messages.impl.timeline.components.customreaction.EmojibaseProvider + +class FakeEmojibaseProvider: EmojibaseProvider { + override fun loadEmojibase(): EmojibaseStore { + return EmojibaseStore(mapOf()) + } +}