From 5e5063981519ff1fcf39072d85bd6bb5ded714fd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 28 Aug 2023 17:20:50 +0200 Subject: [PATCH 01/75] version++ --- plugins/src/main/kotlin/Versions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index ebccf62e9c..0e9d807014 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -56,7 +56,7 @@ private const val versionMinor = 1 // Note: even values are reserved for regular release, odd values for hotfix release. // When creating a hotfix, you should decrease the value, since the current value // is the value for the next regular release. -private const val versionPatch = 5 +private const val versionPatch = 6 object Versions { val versionCode = 4_000_000 + versionMajor * 1_00_00 + versionMinor * 1_00 + versionPatch From ce4c12ce74eb45978e04e53b9cfc3acc8ebb76dd Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 28 Aug 2023 16:45:42 +0100 Subject: [PATCH 02/75] Integrate emojibase - Integrate emojibase datasource - Use element category translations - Use Material emoji category logos --- app/build.gradle.kts | 2 +- .../element/android/x/ElementXApplication.kt | 2 - .../android/x/initializer/EmojiInitializer.kt | 29 ---------- features/messages/impl/build.gradle.kts | 2 +- .../messages/impl/MessagesStateProvider.kt | 3 + .../features/messages/impl/MessagesView.kt | 8 ++- .../CustomReactionBottomSheet.kt | 13 +++-- .../customreaction/CustomReactionEvents.kt | 3 +- .../customreaction/CustomReactionPresenter.kt | 34 +++++++++-- .../customreaction/CustomReactionState.kt | 3 + .../{ => customreaction}/EmojiPicker.kt | 27 +++++---- .../customreaction/EmojibaseExtensions.kt | 58 +++++++++++++++++++ .../CustomReactionPresenterTests.kt | 4 +- gradle/libs.versions.toml | 5 +- 14 files changed, 130 insertions(+), 63 deletions(-) delete mode 100644 app/src/main/kotlin/io/element/android/x/initializer/EmojiInitializer.kt rename features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/{ => customreaction}/EmojiPicker.kt (83%) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19bb2ea84b..405054d837 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -220,7 +220,7 @@ dependencies { implementation(libs.network.okhttp.logging) implementation(libs.serialization.json) - implementation(libs.vanniktech.emoji) + implementation(libs.matrix.emojibase.bindings) implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt index da8592771c..542ae7090d 100644 --- a/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt +++ b/app/src/main/kotlin/io/element/android/x/ElementXApplication.kt @@ -23,7 +23,6 @@ import io.element.android.x.di.AppComponent import io.element.android.x.di.DaggerAppComponent import io.element.android.x.info.logApplicationInfo import io.element.android.x.initializer.CrashInitializer -import io.element.android.x.initializer.EmojiInitializer import io.element.android.x.initializer.TracingInitializer class ElementXApplication : Application(), DaggerComponentOwner { @@ -39,7 +38,6 @@ class ElementXApplication : Application(), DaggerComponentOwner { AppInitializer.getInstance(this).apply { initializeComponent(CrashInitializer::class.java) initializeComponent(TracingInitializer::class.java) - initializeComponent(EmojiInitializer::class.java) } logApplicationInfo() } diff --git a/app/src/main/kotlin/io/element/android/x/initializer/EmojiInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/EmojiInitializer.kt deleted file mode 100644 index dd1e7455c6..0000000000 --- a/app/src/main/kotlin/io/element/android/x/initializer/EmojiInitializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.x.initializer - -import androidx.startup.Initializer -import com.vanniktech.emoji.EmojiManager -import com.vanniktech.emoji.google.GoogleEmojiProvider - -class EmojiInitializer : Initializer { - override fun create(context: android.content.Context) { - EmojiManager.install(GoogleEmojiProvider()) - } - - override fun dependencies(): MutableList>> = mutableListOf() -} diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 948bac4c57..bd8cbc3a11 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -60,7 +60,7 @@ dependencies { implementation(libs.accompanist.systemui) implementation(libs.vanniktech.blurhash) implementation(libs.telephoto.zoomableimage) - implementation(libs.vanniktech.emoji) + implementation(libs.matrix.emojibase.bindings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) 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 7da67d468c..6541d8abc8 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 @@ -16,7 +16,9 @@ package io.element.android.features.messages.impl +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemList @@ -67,6 +69,7 @@ fun aMessagesState() = MessagesState( actionListState = anActionListState(), customReactionState = CustomReactionState( selectedEventId = null, + emojiProvider = Async.Uninitialized, eventSink = {}, ), reactionSummaryState = ReactionSummaryState( 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 34ac14af57..9190646ffc 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 @@ -135,7 +135,8 @@ fun MessagesView( } fun onMoreReactionsClicked(event: TimelineItem.Event) { - state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) + if (event.eventId == null) return + state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId)) } Scaffold( @@ -187,7 +188,8 @@ fun MessagesView( state = state.actionListState, onActionSelected = ::onActionSelected, onCustomReactionClicked = { event -> - state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) + if (event.eventId == null) return@ActionListView + state.customReactionState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(event.eventId)) }, onEmojiReactionClicked = ::onEmojiReactionClicked, ) @@ -197,7 +199,7 @@ fun MessagesView( onEmojiSelected = { emoji -> state.customReactionState.selectedEventId?.let { eventId -> state.eventSink(MessagesEvents.ToggleReaction(emoji.unicode, eventId)) - state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + 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 70c2a7dc10..3bfe42cecf 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 @@ -22,8 +22,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -import com.vanniktech.emoji.Emoji -import io.element.android.features.messages.impl.timeline.components.EmojiPicker +import io.element.android.emojibasebindings.Emoji import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.hide @@ -38,18 +37,19 @@ fun CustomReactionBottomSheet( val coroutineScope = rememberCoroutineScope() fun onDismiss() { - state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + state.eventSink(CustomReactionEvents.DismissCustomReactionSheet) } fun onEmojiSelectedDismiss(emoji: Emoji) { sheetState.hide(coroutineScope) { - state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + state.eventSink(CustomReactionEvents.DismissCustomReactionSheet) onEmojiSelected(emoji) } } - val isVisible = state.selectedEventId != null - if (isVisible) { + val emojiProvider = state.emojiProvider.dataOrNull() + val selectedEventId = state.selectedEventId + if (emojiProvider != null && selectedEventId != null) { ModalBottomSheet( onDismissRequest = ::onDismiss, sheetState = sheetState, @@ -57,6 +57,7 @@ fun CustomReactionBottomSheet( ) { EmojiPicker( onEmojiSelected = ::onEmojiSelectedDismiss, + emojiProvider = emojiProvider, modifier = Modifier.fillMaxSize() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt index b7c210553e..1126474b8a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt @@ -19,5 +19,6 @@ package io.element.android.features.messages.impl.timeline.components.customreac import io.element.android.libraries.matrix.api.core.EventId sealed interface CustomReactionEvents { - data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents + data class ShowCustomReactionSheet(val eventId: EventId) : CustomReactionEvents + object DismissCustomReactionSheet : CustomReactionEvents } 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 0a23d42085..a000a780ae 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 @@ -20,9 +20,15 @@ import androidx.compose.runtime.Composable 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.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.core.EventId +import kotlinx.coroutines.launch import javax.inject.Inject class CustomReactionPresenter @Inject constructor() : Presenter { @@ -30,13 +36,31 @@ class CustomReactionPresenter @Inject constructor() : Presenter(null) } - - fun handleEvents(event: CustomReactionEvents) { - when (event) { - is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId + var emojiState: Async by remember { + mutableStateOf(Async.Uninitialized) + } + val localCoroutineScope = rememberCoroutineScope() + val context = LocalContext.current + fun handleShowCustomReactionSheet(eventId: EventId) { + selectedEventId = eventId + emojiState = Async.Loading() + localCoroutineScope.launch { + emojiState = Async.Success(EmojibaseDatasource().load(context)) } } - return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents) + fun handleDismissCustomReactionSheet() { + selectedEventId = null + emojiState = Async.Uninitialized + } + + fun handleEvents(event: CustomReactionEvents) { + when (event) { + is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.eventId) + is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet() + } + } + + return CustomReactionState(selectedEventId = selectedEventId, emojiProvider = emojiState, 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 6c0c7f3599..9641f2d31a 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 @@ -16,9 +16,12 @@ package io.element.android.features.messages.impl.timeline.components.customreaction +import io.element.android.emojibasebindings.EmojibaseStore +import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.EventId data class CustomReactionState( val selectedEventId: EventId?, + val emojiProvider: Async, val eventSink: (CustomReactionEvents) -> Unit, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt similarity index 83% rename from features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt rename to features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt index 6e121685f2..1fa238d5ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojiPicker.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.messages.impl.timeline.components +package io.element.android.features.messages.impl.timeline.components.customreaction import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable @@ -39,10 +39,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.vanniktech.emoji.Emoji -import com.vanniktech.emoji.google.GoogleEmojiProvider +import io.element.android.emojibasebindings.Emoji +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.emojibasebindings.EmojibaseDatasource +import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon @@ -54,23 +58,22 @@ import kotlinx.coroutines.launch @Composable fun EmojiPicker( onEmojiSelected: (Emoji) -> Unit, + emojiProvider: EmojibaseStore, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - - val emojiProvider = remember { GoogleEmojiProvider() } val categories = remember { emojiProvider.categories } val pagerState = rememberPagerState() Column(modifier) { TabRow( selectedTabIndex = pagerState.currentPage, ) { - categories.forEachIndexed { index, category -> + EmojibaseCategory.values().forEachIndexed { index, category -> Tab( text = { Icon( - resourceId = emojiProvider.getIcon(category), - contentDescription = category.categoryNames["en"] + imageVector = category.icon, + contentDescription = stringResource(id = category.title) ) }, selected = pagerState.currentPage == index, @@ -82,18 +85,19 @@ fun EmojiPicker( } HorizontalPager( - pageCount = categories.size, + pageCount = EmojibaseCategory.values().size, state = pagerState, modifier = Modifier.fillMaxWidth(), ) { index -> - val category = categories[index] + val category = EmojibaseCategory.values()[index] + val emojis = categories[category] ?: listOf() LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = GridCells.Adaptive(minSize = 40.dp), contentPadding = PaddingValues(vertical = 10.dp, horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceEvenly, ) { - items(category.emojis, key = { it.unicode }) { item -> + items(emojis, key = { it.unicode }) { item -> Box( modifier = Modifier .size(40.dp) @@ -132,6 +136,7 @@ internal fun EmojiPickerDarkPreview() { private fun ContentToPreview() { EmojiPicker( onEmojiSelected = {}, + emojiProvider = EmojibaseDatasource().load(LocalContext.current), modifier = Modifier.fillMaxWidth() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt new file mode 100644 index 0000000000..c6e54953e6 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt @@ -0,0 +1,58 @@ +/* + * 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 androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.EmojiEvents +import androidx.compose.material.icons.outlined.EmojiFlags +import androidx.compose.material.icons.outlined.EmojiFoodBeverage +import androidx.compose.material.icons.outlined.EmojiNature +import androidx.compose.material.icons.outlined.EmojiObjects +import androidx.compose.material.icons.outlined.EmojiPeople +import androidx.compose.material.icons.outlined.EmojiSymbols +import androidx.compose.material.icons.outlined.EmojiTransportation +import androidx.compose.ui.graphics.vector.ImageVector +import io.element.android.emojibasebindings.EmojibaseCategory +import io.element.android.libraries.ui.strings.R + +@get:StringRes +val EmojibaseCategory.title: Int get() = + when(this){ + EmojibaseCategory.People -> R.string.emoji_picker_category_people + EmojibaseCategory.Nature -> R.string.emoji_picker_category_nature + EmojibaseCategory.Foods -> R.string.emoji_picker_category_foods + EmojibaseCategory.Activity -> R.string.emoji_picker_category_activity + EmojibaseCategory.Places -> R.string.emoji_picker_category_places + EmojibaseCategory.Objects -> R.string.emoji_picker_category_objects + EmojibaseCategory.Symbols -> R.string.emoji_picker_category_symbols + EmojibaseCategory.Flags -> R.string.emoji_picker_category_flags + } + +val EmojibaseCategory.icon: ImageVector + get() = + when(this){ + EmojibaseCategory.People -> Icons.Outlined.EmojiPeople + EmojibaseCategory.Nature -> Icons.Outlined.EmojiNature + EmojibaseCategory.Foods -> Icons.Outlined.EmojiFoodBeverage + EmojibaseCategory.Activity -> Icons.Outlined.EmojiEvents + EmojibaseCategory.Places -> Icons.Outlined.EmojiTransportation + EmojibaseCategory.Objects -> Icons.Outlined.EmojiObjects + EmojibaseCategory.Symbols -> Icons.Outlined.EmojiSymbols + EmojibaseCategory.Flags -> Icons.Outlined.EmojiFlags + } + 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 1c40483ffe..0ea4ab79c8 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 @@ -38,10 +38,10 @@ class CustomReactionPresenterTests { val initialState = awaitItem() assertThat(initialState.selectedEventId).isNull() - initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID)) + initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(AN_EVENT_ID)) assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID) - initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + initialState.eventSink(CustomReactionEvents.DismissCustomReactionSheet) assertThat(awaitItem().selectedEventId).isNull() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f3540a2e8..c4f5c8ae4f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -154,7 +154,6 @@ sqlite = "androidx.sqlite:sqlite:2.3.1" unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5" vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0" -vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.2.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" @@ -166,6 +165,9 @@ posthog = "com.posthog.android:posthog:2.0.3" sentry = "io.sentry:sentry-android:6.28.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8" +# Emojibase +matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.0.5" + # Di inject = "javax.inject:javax.inject:1" dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } @@ -177,7 +179,6 @@ anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.r google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" } - # Miscellaneous # Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the # value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion. From a3e9f666140431c8dc7069de5a55cba278598974 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 28 Aug 2023 18:11:17 +0100 Subject: [PATCH 03/75] lint --- .../features/messages/impl/MessagesStateProvider.kt | 2 -- .../customreaction/CustomReactionBottomSheet.kt | 2 +- .../components/customreaction/CustomReactionEvents.kt | 2 +- .../customreaction/CustomReactionPresenter.kt | 11 +++++++---- .../timeline/components/customreaction/EmojiPicker.kt | 9 ++++----- 5 files changed, 13 insertions(+), 13 deletions(-) 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 3423830f08..ecc9b1746a 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 @@ -16,9 +16,7 @@ package io.element.android.features.messages.impl -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemList 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 bb02c36e31..05c2956316 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 @@ -57,9 +57,9 @@ fun CustomReactionBottomSheet( ) { EmojiPicker( onEmojiSelected = ::onEmojiSelectedDismiss, - modifier = Modifier.fillMaxSize(), emojiProvider = emojiProvider, selectedEmojis = state.selectedEmoji, + modifier = Modifier.fillMaxSize(), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt index f9da2986d9..2458686a83 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt @@ -19,6 +19,6 @@ package io.element.android.features.messages.impl.timeline.components.customreac import io.element.android.features.messages.impl.timeline.model.TimelineItem sealed interface CustomReactionEvents { - data class ShowCustomReactionSheet(val event: TimelineItem.Event?) : CustomReactionEvents + data class ShowCustomReactionSheet(val event: TimelineItem.Event) : CustomReactionEvents object DismissCustomReactionSheet : CustomReactionEvents } 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 0aa1d06059..4ab94f1656 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 @@ -27,10 +27,8 @@ import io.element.android.emojibasebindings.EmojibaseDatasource import io.element.android.emojibasebindings.EmojibaseStore import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.matrix.api.core.EventId import kotlinx.coroutines.launch import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.libraries.architecture.Presenter import kotlinx.collections.immutable.toImmutableSet import javax.inject.Inject @@ -59,11 +57,16 @@ class CustomReactionPresenter @Inject constructor() : Presenter handleShowCustomReactionSheet(event) + is CustomReactionEvents.ShowCustomReactionSheet -> handleShowCustomReactionSheet(event.event) is CustomReactionEvents.DismissCustomReactionSheet -> handleDismissCustomReactionSheet() } } val selectedEmoji = selectedEvent?.reactionsState?.reactions?.mapNotNull { if(it.isHighlighted) it.key else null }.orEmpty().toImmutableSet() - return CustomReactionState(selectedEventId = selectedEvent?.eventId, emojiProvider = emojiState, selectedEmoji = selectedEmoji, eventSink = ::handleEvents) + return CustomReactionState( + selectedEventId = selectedEvent?.eventId, + emojiProvider = emojiState, + selectedEmoji = selectedEmoji, + eventSink = ::handleEvents + ) } } 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 c2259bdf09..edbd6aabc6 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,13 +63,13 @@ import kotlinx.coroutines.launch @Composable fun EmojiPicker( onEmojiSelected: (Emoji) -> Unit, - modifier: Modifier = Modifier, emojiProvider: EmojibaseStore, selectedEmojis: ImmutableSet, + modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() val categories = remember { emojiProvider.categories } - val pagerState = rememberPagerState(pageCount = { emojiProvider.categories.size }) + val pagerState = rememberPagerState(pageCount = { EmojibaseCategory.values().size }) Column(modifier) { TabRow( selectedTabIndex = pagerState.currentPage, @@ -91,7 +91,6 @@ fun EmojiPicker( } HorizontalPager( - pageCount = EmojibaseCategory.values().size, state = pagerState, modifier = Modifier.fillMaxWidth(), ) { index -> @@ -150,8 +149,8 @@ internal fun EmojiPickerDarkPreview() { private fun ContentToPreview() { EmojiPicker( onEmojiSelected = {}, - modifier = Modifier.fillMaxWidth(), emojiProvider = EmojibaseDatasource().load(LocalContext.current), - selectedEmojis = persistentSetOf("😀", "😄", "😃") + selectedEmojis = persistentSetOf("😀", "😄", "😃"), + modifier = Modifier.fillMaxWidth(), ) } From 6a9152160496a135c87dbe1b75c5ed100518f55a Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 28 Aug 2023 18:20:48 +0100 Subject: [PATCH 04/75] Use CommonStrings --- .../customreaction/EmojibaseExtensions.kt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt index c6e54953e6..fb111cce97 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/EmojibaseExtensions.kt @@ -28,19 +28,19 @@ import androidx.compose.material.icons.outlined.EmojiSymbols import androidx.compose.material.icons.outlined.EmojiTransportation import androidx.compose.ui.graphics.vector.ImageVector import io.element.android.emojibasebindings.EmojibaseCategory -import io.element.android.libraries.ui.strings.R +import io.element.android.libraries.ui.strings.CommonStrings @get:StringRes val EmojibaseCategory.title: Int get() = when(this){ - EmojibaseCategory.People -> R.string.emoji_picker_category_people - EmojibaseCategory.Nature -> R.string.emoji_picker_category_nature - EmojibaseCategory.Foods -> R.string.emoji_picker_category_foods - EmojibaseCategory.Activity -> R.string.emoji_picker_category_activity - EmojibaseCategory.Places -> R.string.emoji_picker_category_places - EmojibaseCategory.Objects -> R.string.emoji_picker_category_objects - EmojibaseCategory.Symbols -> R.string.emoji_picker_category_symbols - EmojibaseCategory.Flags -> R.string.emoji_picker_category_flags + EmojibaseCategory.People -> CommonStrings.emoji_picker_category_people + EmojibaseCategory.Nature -> CommonStrings.emoji_picker_category_nature + EmojibaseCategory.Foods -> CommonStrings.emoji_picker_category_foods + EmojibaseCategory.Activity -> CommonStrings.emoji_picker_category_activity + EmojibaseCategory.Places -> CommonStrings.emoji_picker_category_places + EmojibaseCategory.Objects -> CommonStrings.emoji_picker_category_objects + EmojibaseCategory.Symbols -> CommonStrings.emoji_picker_category_symbols + EmojibaseCategory.Flags -> CommonStrings.emoji_picker_category_flags } val EmojibaseCategory.icon: ImageVector From 1d57d4500c620919eb0ec97d12aaa20636cd66eb Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 28 Aug 2023 20:15:00 +0100 Subject: [PATCH 05/75] Fix test compilation. --- .../components/customreaction/CustomReactionPresenterTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8246520d7c..9918a67296 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 @@ -57,7 +57,7 @@ class CustomReactionPresenterTests { assertThat(initialState.selectedEventId).isNull() val reactions = aTimelineItemReactions(count = 1, isHighlighted = true) val key = reactions.reactions.first().key - initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions))) + initialState.eventSink(CustomReactionEvents.ShowCustomReactionSheet(aTimelineItemEvent(eventId = AN_EVENT_ID, timelineItemReactions = reactions))) val stateWithSelectedEmojis = awaitItem() assertThat(stateWithSelectedEmojis.selectedEventId).isEqualTo(AN_EVENT_ID) assertThat(stateWithSelectedEmojis.selectedEmoji).contains(key) From 6596f5ec483ea800b900ddeb99c701874c4dd056 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 19:24:00 +0000 Subject: [PATCH 06/75] Update gradle/gradle-build-action action to v2.8.0 --- .github/workflows/build.yml | 2 +- .github/workflows/nightlyReports.yml | 2 +- .github/workflows/quality.yml | 2 +- .github/workflows/recordScreenshots.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/sonar.yml | 2 +- .github/workflows/tests.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b8549fc42..1df2119bf9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index e85fdc31d9..65f3cbb53c 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -62,7 +62,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 80ab156480..9c0aac7aef 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -40,7 +40,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 63b72b3318..e9ea93619c 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -24,7 +24,7 @@ jobs: java-version: '17' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 225dc4905e..7e535a1467 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 - name: Create app bundle env: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 1cbbfb639e..42846c2cd5 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -32,7 +32,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: 🔊 Publish results to Sonar diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0094f34e4c..460e57b4e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.7.1 + uses: gradle/gradle-build-action@v2.8.0 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} From c280b43ecc6e7bf46af7cf72f44de168f6ee9457 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 29 Aug 2023 09:59:01 +0200 Subject: [PATCH 07/75] Bump telephoto to 0.6.0-SNAPSHOT to diagnose crash (#1164) --- gradle/libs.versions.toml | 2 +- settings.gradle.kts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 118e6324c5..84bc4c1774 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ dependencycheck = "8.4.0" dependencyanalysis = "1.21.0" stem = "2.3.0" sqldelight = "1.5.5" -telephoto = "0.5.0" +telephoto = "0.6.0-SNAPSHOT" # DI dagger = "2.47" diff --git a/settings.gradle.kts b/settings.gradle.kts index 751c65d388..e1894c16d7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,6 +29,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = URI("https://oss.sonatype.org/content/repositories/snapshots/") } maven { url = URI("https://www.jitpack.io") content { From f8417bda21cc0af7f26cc9ca1a1677800aa02354 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 10:05:59 +0200 Subject: [PATCH 08/75] Format file. --- .../kotlin/io/element/android/x/MainNode.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index 44006a6da5..ce894ec2d6 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -28,8 +28,8 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin import io.element.android.appnav.LoggedInAppScopeFlowNode -import io.element.android.appnav.room.RoomLoadedFlowNode import io.element.android.appnav.RootFlowNode +import io.element.android.appnav.room.RoomLoadedFlowNode import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.DaggerComponentOwner @@ -45,15 +45,14 @@ class MainNode( buildContext: BuildContext, private val mainDaggerComponentOwner: MainDaggerComponentsOwner, plugins: List, -) : - ParentNode( - navModel = PermanentNavModel( - navTargets = setOf(RootNavTarget), - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(RootNavTarget), + savedStateMap = buildContext.savedStateMap, ), + buildContext = buildContext, + plugins = plugins, +), DaggerComponentOwner by mainDaggerComponentOwner { private val loggedInFlowNodeCallback = object : LoggedInAppScopeFlowNode.LifecycleCallback { From 79d4a4c1be22048ba1f79dbea7b104105a607d9a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 10:08:59 +0200 Subject: [PATCH 09/75] `LoggedInAppScopeFlowNode` is just having one permanent child: `LoggedInFlowNode`. So no need to have a Backstack here, a ParentNode with PermanentNavModel is enough. --- .../appnav/LoggedInAppScopeFlowNode.kt | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt index 6fbe63c1e8..6a0e60aaaf 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInAppScopeFlowNode.kt @@ -23,16 +23,15 @@ import coil.Coil import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins -import com.bumble.appyx.navmodel.backstack.BackStack import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.inputs @@ -51,9 +50,9 @@ import kotlinx.parcelize.Parcelize class LoggedInAppScopeFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( - backstack = BackStack( - initialElement = NavTarget.Root, +) : ParentNode( + navModel = PermanentNavModel( + navTargets = setOf(NavTarget), savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -63,10 +62,8 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( fun onOpenBugReport() } - sealed interface NavTarget : Parcelable { - @Parcelize - data object Root : NavTarget - } + @Parcelize + object NavTarget : Parcelable interface LifecycleCallback : NodeLifecycleCallback { fun onFlowCreated(identifier: String, client: MatrixClient) @@ -95,31 +92,24 @@ class LoggedInAppScopeFlowNode @AssistedInject constructor( } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { - return when (navTarget) { - NavTarget.Root -> { - val callback = object : LoggedInFlowNode.Callback { - override fun onOpenBugReport() { - plugins().forEach { it.onOpenBugReport() } - } - } - val nodeLifecycleCallbacks = plugins() - createNode(buildContext, nodeLifecycleCallbacks + callback) + val callback = object : LoggedInFlowNode.Callback { + override fun onOpenBugReport() { + plugins().forEach { it.onOpenBugReport() } } } + val nodeLifecycleCallbacks = plugins() + return createNode(buildContext, nodeLifecycleCallbacks + callback) } suspend fun attachSession(): LoggedInFlowNode { - return waitForChildAttached { navTarget -> - navTarget is NavTarget.Root - } + return waitForChildAttached { _ -> true } } @Composable override fun View(modifier: Modifier) { Children( - navModel = backstack, + navModel = navModel, modifier = modifier, - transitionHandler = rememberDefaultTransitionHandler(), ) } } From bbc7974442befcd162345d1d906627edfb1836e5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:04:36 +0200 Subject: [PATCH 10/75] Add documentation to explain how to install the application from a Github release. --- docs/install_from_github_release.md | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/install_from_github_release.md diff --git a/docs/install_from_github_release.md b/docs/install_from_github_release.md new file mode 100644 index 0000000000..98fd6e4d30 --- /dev/null +++ b/docs/install_from_github_release.md @@ -0,0 +1,55 @@ +# Installing Element X Android from a Github Release. + +This document explain how to install Element X Android from a Github Release. + + + +* [Requirements](#requirements) +* [Steps](#steps) +* [I already have the application on my phone](#i-already-have-the-application-on-my-phone) + + + +## Requirements + +The Github release will contain an Android App Bundle (with `aab` extension) file, and not APKs like on the Element Android project. So there is some steps to perform to generate and signed the APKs from the App Bundle. The generated APKs will then be selected depending on your phone configuration to be installed on a device (emulator or real device). + +The easiest way to do that is to use the debug signature that is shared between the developers and stored in the project. So we recommend to checkout the project first. Android Studio is not required, you can use the command line. + +You will also need to install [bundletool](https://developer.android.com/studio/command-line/bundletool). On MacOS, you can run the following command: + +```bash +brew install bundletool +``` + +## Steps + +1. Open the GitHub release that you want to install from https://github.com/vector-im/element-x-android/releases; +2. Download the asset `app-release-signed.aab` +3. Navigate to the folder where you cloned the project and run the following command: +```bash +bundletool build-apks --bundle= --output=./tmp/elementx.apks \ + --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ + --overwrite +``` +For instance: +```bash +bundletool build-apks --bundle=./tmp/Element/0.1.5/app-release-signed.aab --output=./tmp/elementx.apks \ + --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ + --overwrite +``` +4. Run an Android emulator, or connect a real device to your computer. +5. Install the APKs on the device: +```bash +bundletool install-apks --apks=./tmp/elementx.apks +``` + +That's it, the application should be installed on your device, you can start it from the launcher icon. + +## I already have the application on my phone + +If the application was already installed on your phone, there are several cases: + +- it was installed from the PlayStore, you will have to uninstall it first, because the signature will not match. +- it was installed from a previous GitHub release, this is like an application upgrade, so no need to uninstall the existing app. +- it was installed from a more recent GitHub release, you will have to uninstall it first. From f44fa4190f7b9eb0c0b242118a6e8004bde41a89 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:06:17 +0200 Subject: [PATCH 11/75] Release script: do not bundle the minimal app when checking if the project compiles locally. --- tools/release/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 20005d2816..43d129fa26 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -143,7 +143,7 @@ git commit -a -m "Setting version for the release ${version}" printf "\n================================================================================\n" printf "Building the bundle locally first...\n" -./gradlew clean bundleRelease +./gradlew clean app:bundleRelease printf "\n================================================================================\n" printf "Running towncrier...\n" From b581cc932814168dbdbd9e18021e6a9a8b6da890 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:11:35 +0200 Subject: [PATCH 12/75] Release script: split APKs generation and APK deployment into 2 separate steps. --- tools/release/release.sh | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 43d129fa26..72d75735fd 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -269,19 +269,26 @@ printf "\n====================================================================== printf "The file ${signedBundlePath} has been signed and can be uploaded to the PlayStore!\n" printf "\n================================================================================\n" -read -p "Do you want to install the application to your device? Make sure there is a connected device first. (yes/no) default to yes " doDeploy -doDeploy=${doDeploy:-yes} +read -p "Do you want to build the APKs from the app bundle? You need to do this step if you want to install the application to your device. (yes/no) default to yes " doBuildApks +doBuildApks=${doBuildApks:-yes} -if [ ${doDeploy} == "yes" ]; then +if [ ${doBuildApks} == "yes" ]; then printf "Building apks...\n" bundletool build-apks --bundle=${signedBundlePath} --output=${targetPath}/elementx.apks \ --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ --overwrite - printf "Installing apk for your device...\n" - bundletool install-apks --apks=${targetPath}/elementx.apks - read -p "Please run the application on your phone to check that the upgrade went well (no init sync, etc.). Press enter when it's done." + + read -p "Do you want to install the application to your device? Make sure there is one (and only one!) connected device first. (yes/no) default to yes " doDeploy + doDeploy=${doDeploy:-yes} + if [ ${doDeploy} == "yes" ]; then + printf "Installing apk for your device...\n" + bundletool install-apks --apks=${targetPath}/elementx.apks + read -p "Please run the application on your phone to check that the upgrade went well. Press enter when it's done." + else + printf "APK will not be deployed!\n" + fi else - printf "Apk will not be deployed!\n" + printf "APKs will not be generated!\n" fi printf "\n================================================================================\n" From 34a85c49569b4423978807211c86c8bd4b2b1cd5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:14:33 +0200 Subject: [PATCH 13/75] Release script: Add link to documentation to install the application from the GitHub release. --- tools/release/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 72d75735fd..6d812b42b7 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -303,7 +303,7 @@ read -p ". Press enter when it's done. " printf "\n================================================================================\n" printf "Message for the Android internal room:\n\n" -message="@room Element X Android ${version} is ready to be tested. You can get it from https://github.com/vector-im/element-x-android/releases/tag/v${version}. Please report any feedback here. Thanks!" +message="@room Element X Android ${version} is ready to be tested. You can get it from https://github.com/vector-im/element-x-android/releases/tag/v${version}. Installation instructions can be found [here](https://github.com/vector-im/element-x-android/blob/develop/docs/install_from_github_release.md). Please report any feedback. Thanks!" printf "${message}\n\n" if [[ -z "${elementBotToken}" ]]; then From 614e8fcff9cbc3e084a103f4c8549881b21b4d2e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:19:32 +0200 Subject: [PATCH 14/75] Release script: improve prompt messages. --- tools/release/release.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/release/release.sh b/tools/release/release.sh index 6d812b42b7..a5701ea5e1 100755 --- a/tools/release/release.sh +++ b/tools/release/release.sh @@ -150,7 +150,7 @@ printf "Running towncrier...\n" yes | towncrier build --version "v${version}" printf "\n================================================================================\n" -read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter when it's done." +read -p "Check the file CHANGES.md consistency. It's possible to reorder items (most important changes first) or change their section if relevant. Also an opportunity to fix some typo, or rewrite things. Do not commit your change. Press enter to continue. " # Get the changes to use it to create the GitHub release changelogUrlEncoded=`git diff CHANGES.md | grep ^+ | tail -n +2 | cut -c2- | jq -sRr @uri | sed s/\(/%28/g | sed s/\)/%29/g` @@ -168,7 +168,7 @@ fastlaneFile="4${versionMajor2Digits}${versionMinor2Digits}${versionPatch2Digits fastlanePathFile="./fastlane/metadata/android/en-US/changelogs/${fastlaneFile}" printf "Main changes in this version: TODO.\nFull changelog: https://github.com/vector-im/element-x-android/releases" > ${fastlanePathFile} -read -p "I have created the file ${fastlanePathFile}, please edit it and press enter when it's done." +read -p "I have created the file ${fastlanePathFile}, please edit it and press enter to continue. " git add ${fastlanePathFile} git commit -a -m "Adding fastlane file for version ${version}" @@ -200,7 +200,7 @@ sed "s/private const val versionPatch = .*/private const val versionPatch = ${ne rm ${versionsFileBak} printf "\n================================================================================\n" -read -p "I have updated the versions to prepare the next release, please check that the change are correct and press enter so I can commit." +read -p "I have updated the versions to prepare the next release, please check that the change are correct and press enter so I can commit. " printf "Committing...\n" git commit -a -m 'version++' @@ -263,7 +263,7 @@ printf "Version name: " bundletool dump manifest --bundle=${signedBundlePath} --xpath=/manifest/@android:versionName printf "\n" -read -p "Does it look correct? Press enter when it's done." +read -p "Does it look correct? Press enter to continue. " printf "\n================================================================================\n" printf "The file ${signedBundlePath} has been signed and can be uploaded to the PlayStore!\n" @@ -283,7 +283,7 @@ if [ ${doBuildApks} == "yes" ]; then if [ ${doDeploy} == "yes" ]; then printf "Installing apk for your device...\n" bundletool install-apks --apks=${targetPath}/elementx.apks - read -p "Please run the application on your phone to check that the upgrade went well. Press enter when it's done." + read -p "Please run the application on your phone to check that the upgrade went well. Press enter to continue. " else printf "APK will not be deployed!\n" fi @@ -299,7 +299,7 @@ printf "Then\n" printf " - copy paste the section of the file CHANGES.md for this release (if not there yet)\n" printf " - click on the 'Generate releases notes' button\n" printf " - Add the file ${signedBundlePath} to the GitHub release.\n" -read -p ". Press enter when it's done. " +read -p ". Press enter to continue. " printf "\n================================================================================\n" printf "Message for the Android internal room:\n\n" @@ -307,7 +307,7 @@ message="@room Element X Android ${version} is ready to be tested. You can get i printf "${message}\n\n" if [[ -z "${elementBotToken}" ]]; then - read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter when it's done " + read -p "ELEMENT_BOT_MATRIX_TOKEN is not defined in the environment. Cannot send the message for you. Please send it manually, and press enter to continue. " else read -p "Send this message to the room (yes/no) default to yes? " doSend doSend=${doSend:-yes} From eb041277a9ef0dc8ae9ef30a5065188fa0b890ee Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 11:22:14 +0200 Subject: [PATCH 15/75] Grammar. --- docs/install_from_github_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install_from_github_release.md b/docs/install_from_github_release.md index 98fd6e4d30..eec5586d82 100644 --- a/docs/install_from_github_release.md +++ b/docs/install_from_github_release.md @@ -1,6 +1,6 @@ # Installing Element X Android from a Github Release. -This document explain how to install Element X Android from a Github Release. +This document explains how to install Element X Android from a Github Release. From 9c6a5bed540993c06bf700c12f762ef4b9a570bb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 28 Aug 2023 18:22:26 +0200 Subject: [PATCH 16/75] Add unit tests for TimelineItemContentPollFactory --- .../TimelineItemContentPollFactoryTest.kt | 288 ++++++++++++++++++ .../android/libraries/matrix/test/TestData.kt | 8 + 2 files changed, 296 insertions(+) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt new file mode 100644 index 0000000000..b7d13784f8 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -0,0 +1,288 @@ +/* + * 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.factories.event + +import com.google.common.truth.Truth +import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentPollFactory +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent +import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_10 +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import io.element.android.libraries.matrix.test.A_USER_ID_6 +import io.element.android.libraries.matrix.test.A_USER_ID_7 +import io.element.android.libraries.matrix.test.A_USER_ID_8 +import io.element.android.libraries.matrix.test.A_USER_ID_9 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val A_POLL_QUESTION = "What is your favorite food?" +private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") +private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") +private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") +private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") + +internal class TimelineItemContentPollFactoryTest { + + private val factory = TimelineItemContentPollFactory( + matrixClient = FakeMatrixClient(), + featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.Polls.key to true)), + ) + + @Test + fun `Disclosed poll - not ended states`() = runTest { + // No votes + Truth.assertThat( + factory.create(aPollContent(PollKind.Disclosed)) + ).isEqualTo(aTimelineItemPollContent(PollKind.Disclosed)) + + // Some votes, according one from current user + val votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Disclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(votesCount = 3, percentage = 0.3f), + aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(A_POLL_ANSWER_3).copy(votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(votesCount = 1, percentage = 0.1f), + ), + votes = votes, + ) + ) + } + + @Test + fun `Disclosed poll - ended states`() = runTest { + // No votes, no winner + Truth.assertThat( + factory.create(aPollContent(PollKind.Disclosed).copy(endTime = 1UL)) + ).isEqualTo( + aTimelineItemPollContent(PollKind.Disclosed).let { + it.copy( + answerItems = it.answerItems.map { answerItem -> + answerItem.copy(isEnabled = false, isWinner = false) + }, + isEnded = true, + ) + } + ) + + // Some votes, according one from current user (winner) + var votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Disclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(A_POLL_ANSWER_3).copy(isEnabled = false, votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + votes = votes, + isEnded = true, + ) + ) + + // Some votes, according one from current user (not winner) and two winning votes + votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_6), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Disclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(A_POLL_ANSWER_3).copy(isEnabled = false, votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + votes = votes, + isEnded = true, + ) + ) + } + + @Test + fun `Undisclosed poll - not ended states`() = runTest { + // No votes + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy()) + ).isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).let { + it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) + } + ) + + // Some votes, according one from current user + val votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(A_POLL_ANSWER_2).copy(isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = false, votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = false, votesCount = 1, percentage = 0.1f), + ), + votes = votes, + ) + ) + } + + @Test + fun `Undisclosed poll - ended states`() = runTest { + // No votes, no winner + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(endTime = 1UL)) + ).isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).let { + it.copy( + answerItems = it.answerItems.map { answerItem -> + answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false) + }, + isEnded = true, + ) + } + ) + + // Some votes, according one from current user (winner) + var votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = true, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(A_POLL_ANSWER_2).copy( + isDisclosed = true, + isSelected = true, + isEnabled = false, + isWinner = true, + votesCount = 6, + percentage = 0.6f + ), + aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = true, isEnabled = false, votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = true, isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + votes = votes, + isEnded = true, + ) + ) + + // Some votes, according one from current user (not winner) and two winning votes + votes = mapOf( + A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), + A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_6), + A_POLL_ANSWER_3.id to emptyList(), + A_POLL_ANSWER_4.id to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), + ) + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent(PollKind.Undisclosed).copy( + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = true, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(A_POLL_ANSWER_2).copy(isDisclosed = true, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = true, isEnabled = false, votesCount = 0, percentage = 0f), + aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = true, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + votes = votes, + isEnded = true, + ) + ) + } + + private fun aPollContent(pollKind: PollKind): PollContent = PollContent( + question = A_POLL_QUESTION, + kind = pollKind, + maxSelections = 1UL, + answers = listOf( + A_POLL_ANSWER_1, + A_POLL_ANSWER_2, + A_POLL_ANSWER_3, + A_POLL_ANSWER_4, + ), + votes = emptyMap(), + endTime = null, + ) + + private fun aTimelineItemPollContent(pollKind: PollKind) = TimelineItemPollContent( + question = A_POLL_QUESTION, + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1), + aPollAnswerItem(A_POLL_ANSWER_2), + aPollAnswerItem(A_POLL_ANSWER_3), + aPollAnswerItem(A_POLL_ANSWER_4), + ), + votes = emptyMap(), + pollKind = pollKind, + isEnded = false, + ) + + private fun aPollAnswerItem(answer: PollAnswer) = PollAnswerItem( + answer = answer, + isSelected = false, + isEnabled = true, + isWinner = false, + isDisclosed = true, + votesCount = 0, + percentage = 0f, + ) +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 677774afe4..f6b6e1645f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -30,6 +30,14 @@ const val A_PASSWORD = "password" val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") +val A_USER_ID_3 = UserId("@carol:server.org") +val A_USER_ID_4 = UserId("@david:server.org") +val A_USER_ID_5 = UserId("@eve:server.org") +val A_USER_ID_6 = UserId("@justin:server.org") +val A_USER_ID_7 = UserId("@mallory:server.org") +val A_USER_ID_8 = UserId("@susie:server.org") +val A_USER_ID_9 = UserId("@victor:server.org") +val A_USER_ID_10 = UserId("@walter:server.org") val A_SESSION_ID: SessionId = A_USER_ID val A_SESSION_ID_2: SessionId = A_USER_ID_2 val A_SPACE_ID = SpaceId("!aSpaceId:domain") From 992050c8328d1c4e9420d606a2471e530eb6e502 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 29 Aug 2023 15:58:46 +0200 Subject: [PATCH 17/75] Factorize code and remove unused field in TimelineItemPollContent --- .../event/TimelineItemContentPollFactory.kt | 1 - .../model/event/TimelineItemPollContent.kt | 2 - .../event/TimelineItemPollContentProvider.kt | 1 - .../TimelineItemContentPollFactoryTest.kt | 299 +++++++++--------- 4 files changed, 145 insertions(+), 158 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index 4a21874e1c..9c06b17056 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -69,7 +69,6 @@ class TimelineItemContentPollFactory @Inject constructor( return TimelineItemPollContent( question = content.question, answerItems = answerItems, - votes = content.votes, pollKind = content.kind, isEnded = isEndedPoll, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt index 3c0e0edfd4..0f94b97776 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt @@ -17,13 +17,11 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.poll.api.PollAnswerItem -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.poll.PollKind data class TimelineItemPollContent( val question: String, val answerItems: List, - val votes: Map>, val pollKind: PollKind, val isEnded: Boolean, ) : TimelineItemEventContent { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index 49dfa58c8a..247d450ae7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -34,6 +34,5 @@ fun aTimelineItemPollContent(): TimelineItemPollContent { question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), isEnded = false, - votes = emptyMap(), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt index b7d13784f8..a1411293e9 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.timeline.item.event.PollContent @@ -39,12 +40,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import kotlinx.coroutines.test.runTest import org.junit.Test -private const val A_POLL_QUESTION = "What is your favorite food?" -private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") -private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") -private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") -private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") - internal class TimelineItemContentPollFactoryTest { private val factory = TimelineItemContentPollFactory( @@ -53,101 +48,82 @@ internal class TimelineItemContentPollFactoryTest { ) @Test - fun `Disclosed poll - not ended states`() = runTest { - // No votes - Truth.assertThat( - factory.create(aPollContent(PollKind.Disclosed)) - ).isEqualTo(aTimelineItemPollContent(PollKind.Disclosed)) + fun `Disclosed poll - not ended, no votes`() = runTest { + Truth.assertThat(factory.create(aPollContent())).isEqualTo(aTimelineItemPollContent()) + } - // Some votes, according one from current user - val votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), - ) + @Test + fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes)) + factory.create(aPollContent(votes = votes)) ) .isEqualTo( - aTimelineItemPollContent(PollKind.Disclosed).copy( + aTimelineItemPollContent( answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(votesCount = 3, percentage = 0.3f), - aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(A_POLL_ANSWER_3).copy(votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(votesCount = 1, percentage = 0.1f), + aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3), + aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f), ), - votes = votes, ) ) } @Test - fun `Disclosed poll - ended states`() = runTest { - // No votes, no winner + fun `Disclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(PollKind.Disclosed).copy(endTime = 1UL)) + factory.create(aPollContent(endTime = 1UL)) ).isEqualTo( - aTimelineItemPollContent(PollKind.Disclosed).let { + aTimelineItemPollContent().let { it.copy( - answerItems = it.answerItems.map { answerItem -> - answerItem.copy(isEnabled = false, isWinner = false) - }, + answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }, isEnded = true, ) } ) + } - // Some votes, according one from current user (winner) - var votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), - ) + @Test + fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes, endTime = 1UL)) + factory.create(aPollContent(votes = votes, endTime = 1UL)) ) .isEqualTo( - aTimelineItemPollContent(PollKind.Disclosed).copy( + aTimelineItemPollContent( answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(isEnabled = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(A_POLL_ANSWER_3).copy(isEnabled = false, votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(isEnabled = false, votesCount = 1, percentage = 0.1f), + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), ), - votes = votes, - isEnded = true, - ) - ) - - // Some votes, according one from current user (not winner) and two winning votes - votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_6), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), - ) - Truth.assertThat( - factory.create(aPollContent(PollKind.Disclosed).copy(votes = votes, endTime = 1UL)) - ) - .isEqualTo( - aTimelineItemPollContent(PollKind.Disclosed).copy( - answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - aPollAnswerItem(A_POLL_ANSWER_2).copy(isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), - aPollAnswerItem(A_POLL_ANSWER_3).copy(isEnabled = false, votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - ), - votes = votes, isEnded = true, ) ) } @Test - fun `Undisclosed poll - not ended states`() = runTest { - // No votes + fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + ) + } + + @Test + fun `Undisclosed poll - not ended, no votes`() = runTest { Truth.assertThat( factory.create(aPollContent(PollKind.Undisclosed).copy()) ).isEqualTo( @@ -155,38 +131,35 @@ internal class TimelineItemContentPollFactoryTest { it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) } ) + } - // Some votes, according one from current user - val votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), - ) + @Test + fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes)) ) .isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).copy( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(A_POLL_ANSWER_2).copy(isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = false, votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = false, votesCount = 1, percentage = 0.1f), + aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f), ), - votes = votes, ) ) } @Test - fun `Undisclosed poll - ended states`() = runTest { - // No votes, no winner + fun `Undisclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL)) ).isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).let { + aTimelineItemPollContent().let { it.copy( + pollKind = PollKind.Undisclosed, answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false) }, @@ -194,95 +167,113 @@ internal class TimelineItemContentPollFactoryTest { ) } ) + } - // Some votes, according one from current user (winner) - var votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_10), - ) + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL)) ) .isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).copy( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = true, isEnabled = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(A_POLL_ANSWER_2).copy( - isDisclosed = true, - isSelected = true, - isEnabled = false, - isWinner = true, - votesCount = 6, - percentage = 0.6f - ), - aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = true, isEnabled = false, votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = true, isEnabled = false, votesCount = 1, percentage = 0.1f), + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), ), - votes = votes, - isEnded = true, - ) - ) - - // Some votes, according one from current user (not winner) and two winning votes - votes = mapOf( - A_POLL_ANSWER_1.id to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), - A_POLL_ANSWER_2.id to listOf(A_USER_ID, A_USER_ID_6), - A_POLL_ANSWER_3.id to emptyList(), - A_POLL_ANSWER_4.id to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), - ) - Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) - ) - .isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).copy( - answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1).copy(isDisclosed = true, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - aPollAnswerItem(A_POLL_ANSWER_2).copy(isDisclosed = true, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), - aPollAnswerItem(A_POLL_ANSWER_3).copy(isDisclosed = true, isEnabled = false, votesCount = 0, percentage = 0f), - aPollAnswerItem(A_POLL_ANSWER_4).copy(isDisclosed = true, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - ), - votes = votes, isEnded = true, ) ) } - private fun aPollContent(pollKind: PollKind): PollContent = PollContent( + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } + Truth.assertThat( + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + ) + .isEqualTo( + aTimelineItemPollContent( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + ) + } + + private fun aPollContent( + pollKind: PollKind = PollKind.Disclosed, + votes: Map> = emptyMap(), + endTime: ULong? = null, + ): PollContent = PollContent( question = A_POLL_QUESTION, kind = pollKind, maxSelections = 1UL, - answers = listOf( - A_POLL_ANSWER_1, - A_POLL_ANSWER_2, - A_POLL_ANSWER_3, - A_POLL_ANSWER_4, - ), - votes = emptyMap(), - endTime = null, + answers = listOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), + votes = votes, + endTime = endTime, ) - private fun aTimelineItemPollContent(pollKind: PollKind) = TimelineItemPollContent( - question = A_POLL_QUESTION, - answerItems = listOf( + private fun aTimelineItemPollContent( + pollKind: PollKind = PollKind.Disclosed, + answerItems: List = listOf( aPollAnswerItem(A_POLL_ANSWER_1), aPollAnswerItem(A_POLL_ANSWER_2), aPollAnswerItem(A_POLL_ANSWER_3), aPollAnswerItem(A_POLL_ANSWER_4), ), - votes = emptyMap(), + isEnded: Boolean = false, + ) = TimelineItemPollContent( + question = A_POLL_QUESTION, + answerItems = answerItems, pollKind = pollKind, - isEnded = false, + isEnded = isEnded, ) - private fun aPollAnswerItem(answer: PollAnswer) = PollAnswerItem( + private fun aPollAnswerItem( + answer: PollAnswer, + isSelected: Boolean = false, + isEnabled: Boolean = true, + isWinner: Boolean = false, + isDisclosed: Boolean = true, + votesCount: Int = 0, + percentage: Float = 0f, + ) = PollAnswerItem( answer = answer, - isSelected = false, - isEnabled = true, - isWinner = false, - isDisclosed = true, - votesCount = 0, - percentage = 0f, + isSelected = isSelected, + isEnabled = isEnabled, + isWinner = isWinner, + isDisclosed = isDisclosed, + votesCount = votesCount, + percentage = percentage, ) + + private companion object TestData { + private const val A_POLL_QUESTION = "What is your favorite food?" + private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") + private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") + private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") + private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") + + private val MY_USER_WINNING_VOTES = mapOf( + A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner + A_POLL_ANSWER_3 to emptyList(), + A_POLL_ANSWER_4 to listOf(A_USER_ID_10), + ) + private val OTHER_WINNING_VOTES = mapOf( + A_POLL_ANSWER_1 to listOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner + A_POLL_ANSWER_2 to listOf(A_USER_ID /* my vote */, A_USER_ID_6), + A_POLL_ANSWER_3 to emptyList(), + A_POLL_ANSWER_4 to listOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner + ) + } } From 097efc26d262cdeaf29a98e19fbcfaa495ab8786 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:09:26 +0200 Subject: [PATCH 18/75] Migrate AnalyticsPreferencesView from `PreferenceSwitch` to `ListItem`. --- .../preferences/AnalyticsPreferencesView.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 5aa9287d86..017822a825 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -18,7 +18,6 @@ package io.element.android.features.analytics.api.preferences import androidx.annotation.StringRes import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource @@ -28,9 +27,11 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.analytics.api.AnalyticsOptInEvents -import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch +import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.LinkColor import io.element.android.libraries.ui.strings.CommonStrings @@ -50,13 +51,16 @@ fun AnalyticsPreferencesView( ) val subtitle = "$firstPart\n\n$secondPart" - PreferenceSwitch( + ListItem( + headlineContent = { + Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data)) + }, + supportingContent = { + Text(text = subtitle) + }, + leadingContent = null, + trailingContent = ListItemContent.Switch(checked = state.isEnabled, onChange = ::onEnabledChanged), modifier = modifier, - title = stringResource(id = CommonStrings.screen_analytics_settings_share_data), - subtitle = subtitle, - isChecked = state.isEnabled, - onCheckedChange = ::onEnabledChanged, - switchAlignment = Alignment.Top, ) } From 24fc2e77b973e876b7aa3877168502109775f75b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:10:02 +0200 Subject: [PATCH 19/75] Format. --- .../analytics/api/preferences/AnalyticsPreferencesView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 017822a825..433358e4ba 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -79,7 +79,9 @@ fun buildAnnotatedStringWithColoredPart( style = SpanStyle( color = color, textDecoration = if (underline) TextDecoration.Underline else null - ), start = startIndex, end = startIndex + coloredPart.length + ), + start = startIndex, + end = startIndex + coloredPart.length, ) } From 1bf2dc1c4d78fe4000d43de188e8c399feba1954 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:11:08 +0200 Subject: [PATCH 20/75] Fix rendering issue of the link. --- .../analytics/api/preferences/AnalyticsPreferencesView.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 433358e4ba..d268328992 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -49,7 +49,11 @@ fun AnalyticsPreferencesView( CommonStrings.screen_analytics_settings_read_terms, CommonStrings.screen_analytics_settings_read_terms_content_link ) - val subtitle = "$firstPart\n\n$secondPart" + val subtitle = buildAnnotatedString { + append(firstPart) + append("\n\n") + append(secondPart) + } ListItem( headlineContent = { From ff47629f6c7225597febf11a061bba1621cbeffa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:32:29 +0200 Subject: [PATCH 21/75] Make the link open the policy url in the analytics setting screen. --- .../preferences/AnalyticsPreferencesState.kt | 1 + .../AnalyticsPreferencesStateProvider.kt | 1 + .../preferences/AnalyticsPreferencesView.kt | 30 +++++++++++++++++-- .../DefaultAnalyticsPreferencesPresenter.kt | 2 ++ .../AnalyticsPreferencesPresenterTest.kt | 1 + .../impl/analytics/AnalyticsSettingsNode.kt | 11 +++++++ .../impl/analytics/AnalyticsSettingsView.kt | 3 ++ 7 files changed, 47 insertions(+), 2 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt index 7cf0f51dfd..e03796297e 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesState.kt @@ -21,5 +21,6 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents data class AnalyticsPreferencesState( val applicationName: String, val isEnabled: Boolean, + val policyUrl: String, val eventSink: (AnalyticsOptInEvents) -> Unit, ) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt index ea397b4d67..18f902a6fd 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesStateProvider.kt @@ -28,5 +28,6 @@ open class AnalyticsPreferencesStateProvider : PreviewParameterProvider Unit, ) { fun onEnabledChanged(isEnabled: Boolean) { state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled)) @@ -47,7 +49,8 @@ fun AnalyticsPreferencesView( val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName) val secondPart = buildAnnotatedStringWithColoredPart( CommonStrings.screen_analytics_settings_read_terms, - CommonStrings.screen_analytics_settings_read_terms_content_link + CommonStrings.screen_analytics_settings_read_terms_content_link, + link = state.policyUrl, ) val subtitle = buildAnnotatedString { append(firstPart) @@ -60,6 +63,16 @@ fun AnalyticsPreferencesView( Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data)) }, supportingContent = { + ClickableText( + text = subtitle, + onClick = { + subtitle + .getStringAnnotations("link", it, it) + .firstOrNull()?.let { stringAnnotation -> + onOpenAnalyticsPolicy(stringAnnotation.item) + } + } + ) Text(text = subtitle) }, leadingContent = null, @@ -68,12 +81,14 @@ fun AnalyticsPreferencesView( ) } +// TODO Use buildAnnotatedStringWithStyledPart. @Composable fun buildAnnotatedStringWithColoredPart( @StringRes fullTextRes: Int, @StringRes coloredTextRes: Int, color: Color = LinkColor, underline: Boolean = true, + link: String? = null, ) = buildAnnotatedString { val coloredPart = stringResource(coloredTextRes) val fullText = stringResource(fullTextRes, coloredPart) @@ -87,6 +102,14 @@ fun buildAnnotatedStringWithColoredPart( start = startIndex, end = startIndex + coloredPart.length, ) + if (link != null) { + addStringAnnotation( + tag = "link", + annotation = link, + start = startIndex, + end = startIndex + coloredPart.length + ) + } } @Preview @@ -101,5 +124,8 @@ internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPref @Composable private fun ContentToPreview(state: AnalyticsPreferencesState) { - AnalyticsPreferencesView(state) + AnalyticsPreferencesView( + state = state, + onOpenAnalyticsPolicy = {}, + ) } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt index 6debe4c232..06431402a6 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/preferences/DefaultAnalyticsPreferencesPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.analytics.api.AnalyticsOptInEvents +import io.element.android.features.analytics.api.Config import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesPresenter import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState import io.element.android.libraries.core.meta.BuildMeta @@ -51,6 +52,7 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor( return AnalyticsPreferencesState( applicationName = buildMeta.applicationName, isEnabled = isEnabled.value, + policyUrl = Config.POLICY_LINK, eventSink = ::handleEvents ) } diff --git a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt index 843e5b5532..29c4579d8f 100644 --- a/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt +++ b/features/analytics/impl/src/test/kotlin/io/element/android/features/analytics/impl/preferences/AnalyticsPreferencesPresenterTest.kt @@ -39,6 +39,7 @@ class AnalyticsPreferencesPresenterTest { skipItems(1) val initialState = awaitItem() assertThat(initialState.isEnabled).isTrue() + assertThat(initialState.policyUrl).isNotEmpty() } } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt index adc917b7e6..18bb99e8e1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt @@ -16,15 +16,19 @@ package io.element.android.features.preferences.impl.analytics +import android.app.Activity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.theme.ElementTheme @ContributesNode(SessionScope::class) class AnalyticsSettingsNode @AssistedInject constructor( @@ -33,12 +37,19 @@ class AnalyticsSettingsNode @AssistedInject constructor( private val presenter: AnalyticsSettingsPresenter, ) : Node(buildContext, plugins = plugins) { + private fun onOpenAnalyticsPolicy(activity: Activity, darkTheme: Boolean, url: String) { + activity.openUrlInChromeCustomTab(null, darkTheme, url) + } + @Composable override fun View(modifier: Modifier) { + val activity = LocalContext.current as Activity + val isDark = ElementTheme.colors.isLight.not() val state = presenter.present() AnalyticsSettingsView( state = state, onBackPressed = ::navigateUp, + onOpenAnalyticsPolicy = { onOpenAnalyticsPolicy(activity, darkTheme = isDark, it) }, modifier = modifier ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt index 3ee7365122..83dd5554fd 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun AnalyticsSettingsView( state: AnalyticsSettingsState, onBackPressed: () -> Unit, + onOpenAnalyticsPolicy: (url: String) -> Unit, modifier: Modifier = Modifier, ) { PreferenceView( @@ -40,6 +41,7 @@ fun AnalyticsSettingsView( ) { AnalyticsPreferencesView( state = state.analyticsState, + onOpenAnalyticsPolicy = onOpenAnalyticsPolicy, ) } } @@ -59,5 +61,6 @@ private fun ContentToPreview(state: AnalyticsSettingsState) { AnalyticsSettingsView( state = state, onBackPressed = {}, + onOpenAnalyticsPolicy = {}, ) } From 79af05bc08eccee7834542d35b4b4b6fd2074269 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:38:21 +0200 Subject: [PATCH 22/75] Use buildAnnotatedStringWithStyledPart and remove copied code. --- .../preferences/AnalyticsPreferencesView.kt | 45 +++---------------- .../designsystem/text/AnnotatedStrings.kt | 10 +++++ 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index e5b40ee8a9..42775ef83f 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -16,26 +16,24 @@ package io.element.android.features.analytics.api.preferences -import androidx.annotation.StringRes import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.theme.LinkColor import io.element.android.libraries.ui.strings.CommonStrings +private const val LINK_TAG = "link" + @Composable fun AnalyticsPreferencesView( state: AnalyticsPreferencesState, @@ -47,10 +45,10 @@ fun AnalyticsPreferencesView( } val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName) - val secondPart = buildAnnotatedStringWithColoredPart( + val secondPart = buildAnnotatedStringWithStyledPart( CommonStrings.screen_analytics_settings_read_terms, CommonStrings.screen_analytics_settings_read_terms_content_link, - link = state.policyUrl, + tagAndLink = LINK_TAG to state.policyUrl, ) val subtitle = buildAnnotatedString { append(firstPart) @@ -67,7 +65,7 @@ fun AnalyticsPreferencesView( text = subtitle, onClick = { subtitle - .getStringAnnotations("link", it, it) + .getStringAnnotations(LINK_TAG, it, it) .firstOrNull()?.let { stringAnnotation -> onOpenAnalyticsPolicy(stringAnnotation.item) } @@ -81,37 +79,6 @@ fun AnalyticsPreferencesView( ) } -// TODO Use buildAnnotatedStringWithStyledPart. -@Composable -fun buildAnnotatedStringWithColoredPart( - @StringRes fullTextRes: Int, - @StringRes coloredTextRes: Int, - color: Color = LinkColor, - underline: Boolean = true, - link: String? = null, -) = buildAnnotatedString { - val coloredPart = stringResource(coloredTextRes) - val fullText = stringResource(fullTextRes, coloredPart) - val startIndex = fullText.indexOf(coloredPart) - append(fullText) - addStyle( - style = SpanStyle( - color = color, - textDecoration = if (underline) TextDecoration.Underline else null - ), - start = startIndex, - end = startIndex + coloredPart.length, - ) - if (link != null) { - addStringAnnotation( - tag = "link", - annotation = link, - start = startIndex, - end = startIndex + coloredPart.length - ) - } -} - @Preview @Composable internal fun AnalyticsPreferencesViewLightPreview(@PreviewParameter(AnalyticsPreferencesStateProvider::class) state: AnalyticsPreferencesState) = diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt index 779b7e7053..04a872a1cf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt @@ -59,6 +59,7 @@ fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { * @param color the color to apply to the string * @param underline whether to underline the string * @param bold whether to bold the string + * @param tagAndLink an optional pair of tag and link to add to the styled part of the string, as StringAnnotation */ @Composable fun buildAnnotatedStringWithStyledPart( @@ -67,6 +68,7 @@ fun buildAnnotatedStringWithStyledPart( color: Color = LinkColor, underline: Boolean = true, bold: Boolean = false, + tagAndLink: Pair? = null, ) = buildAnnotatedString { val coloredPart = stringResource(coloredTextRes) val fullText = stringResource(fullTextRes, coloredPart) @@ -81,6 +83,14 @@ fun buildAnnotatedStringWithStyledPart( start = startIndex, end = startIndex + coloredPart.length, ) + if (tagAndLink != null) { + addStringAnnotation( + tag = tagAndLink.first, + annotation = tagAndLink.second, + start = startIndex, + end = startIndex + coloredPart.length + ) + } } /** From a86100003fb2020b24286a949e0165ce734c3590 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:39:59 +0200 Subject: [PATCH 23/75] Remove preview Text. --- .../analytics/api/preferences/AnalyticsPreferencesView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 42775ef83f..16507ebcfe 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -71,7 +71,6 @@ fun AnalyticsPreferencesView( } } ) - Text(text = subtitle) }, leadingContent = null, trailingContent = ListItemContent.Switch(checked = state.isEnabled, onChange = ::onEnabledChanged), From 36bf343a95ed6a9a901df91185d5d0db7578c3b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:46:42 +0200 Subject: [PATCH 24/75] OptIn screen: make only the `here` word be clickable. --- .../analytics/impl/AnalyticsOptInView.kt | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 54b9add785..bd0dee5f89 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -18,7 +18,6 @@ package io.element.android.features.analytics.impl import androidx.activity.compose.BackHandler import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -28,7 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Poll import androidx.compose.material.icons.rounded.Check @@ -37,7 +36,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -45,6 +43,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.analytics.api.AnalyticsOptInEvents +import io.element.android.features.analytics.api.Config import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem @@ -56,7 +55,6 @@ import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithSt import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial import io.element.android.libraries.designsystem.utils.LogCompositions @@ -98,6 +96,8 @@ fun AnalyticsOptInView( ) } +private const val LINK_TAG = "link" + @Composable private fun AnalyticsOptInHeader( state: AnalyticsOptInState, @@ -114,21 +114,29 @@ private fun AnalyticsOptInHeader( subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), iconImageVector = Icons.Filled.Poll ) - Text( - text = buildAnnotatedStringWithStyledPart( - R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link, - color = Color.Unspecified, - underline = false, - bold = true, - ), + val text = buildAnnotatedStringWithStyledPart( + R.string.screen_analytics_prompt_read_terms, + R.string.screen_analytics_prompt_read_terms_content_link, + color = Color.Unspecified, + underline = false, + bold = true, + tagAndLink = LINK_TAG to Config.POLICY_LINK, + ) + ClickableText( + text = text, + onClick = { + text + .getStringAnnotations(LINK_TAG, it, it) + .firstOrNull() + ?.let { _ -> onClickTerms() } + }, modifier = Modifier - .clip(shape = RoundedCornerShape(8.dp)) - .clickable { onClickTerms() } .padding(8.dp), - style = ElementTheme.typography.fontBodyMdRegular, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.secondary, + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Center, + ) ) } } From 7537772d949e041bf98b54f92118be7df409df06 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:46:59 +0200 Subject: [PATCH 25/75] format. --- .../analytics/api/preferences/AnalyticsPreferencesView.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 16507ebcfe..e93f737d41 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -66,7 +66,8 @@ fun AnalyticsPreferencesView( onClick = { subtitle .getStringAnnotations(LINK_TAG, it, it) - .firstOrNull()?.let { stringAnnotation -> + .firstOrNull() + ?.let { stringAnnotation -> onOpenAnalyticsPolicy(stringAnnotation.item) } } From d3c6efc6661b21fc0e64e63ccc2f07a72936b805 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 16:50:32 +0200 Subject: [PATCH 26/75] Changelog. --- changelog.d/1177.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1177.bugfix diff --git a/changelog.d/1177.bugfix b/changelog.d/1177.bugfix new file mode 100644 index 0000000000..edbf2e9006 --- /dev/null +++ b/changelog.d/1177.bugfix @@ -0,0 +1 @@ +Add missing link to the terms on the analytics setting screen. From ece82203eb06575ebfa83e642a0e8bac4c4ab8f4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 29 Aug 2023 18:01:21 +0200 Subject: [PATCH 27/75] Fix style (typo and color) for supporting content. --- .../analytics/api/preferences/AnalyticsPreferencesView.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index e93f737d41..9920b8a0b0 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -17,6 +17,7 @@ package io.element.android.features.analytics.api.preferences import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -30,6 +31,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.theme.components.ListItem import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings private const val LINK_TAG = "link" @@ -70,7 +72,11 @@ fun AnalyticsPreferencesView( ?.let { stringAnnotation -> onOpenAnalyticsPolicy(stringAnnotation.item) } - } + }, + style = ElementTheme.typography.fontBodyMdRegular + .copy( + color = MaterialTheme.colorScheme.secondary, + ), ) }, leadingContent = null, From 1111b1408ccff16d00a0edd8d5239af5ed535bc1 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 29 Aug 2023 16:13:48 +0000 Subject: [PATCH 28/75] Update screenshots --- ..._AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ull_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png index 16de0b9378..28ae52b035 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a08d5ff1f91c0db7c502882815d36d6a675567ebf8c3eddc0ebff431e3592e67 -size 23390 +oid sha256:f70fad1a0ff08c3bd259ee8abd745d07474fa1dd45cfd2b8c77fe3536fedbb29 +size 23799 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png index 7d6d55ba11..5ce51a0c7a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a5300ac57d9d0137cf82af4ed4bd86c3ab7f3a70d2560954e524b7b3c120199 -size 23515 +oid sha256:07c00693ed47f8db94c0a9bcdd7350e32366813b3a4241dceebbd8aed78a5bbe +size 23880 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png index 7e92a427ab..d12a018f64 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4dd40c1eac2e5f33ce340ce69d8c0f502cd0912a0d511602e1122a6f4d8ae330 -size 25433 +oid sha256:fcde2d058771af909595c4f163925fadc0ddee8cb0c440cbc3268403961e617c +size 25774 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png index 4a8255f3ac..e86823967d 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76dbcb5845565774bd33a192d02a3da0216c34b17e6e1bc1f208cabef9506617 -size 26630 +oid sha256:cef0f004854681538224bd5513e0ec66afb87e54fc7bac4a4eb4c0194c9f594c +size 26965 From 633d5282d6b36d69cee2d2367abc658321104a91 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 29 Aug 2023 22:31:21 +0200 Subject: [PATCH 29/75] "Create poll" UI (#1143) NB: This is missing analytics, which will be added once https://github.com/matrix-org/matrix-analytics-events/pull/85 is merged. Closes https://github.com/vector-im/element-meta/issues/2011 --- changelog.d/1143.feature | 1 + .../messages/impl/MessagesFlowNode.kt | 12 + .../features/messages/impl/MessagesNode.kt | 6 + .../features/messages/impl/MessagesView.kt | 5 + .../impl/actionlist/ActionListPresenter.kt | 17 ++ .../messagecomposer/AttachmentsBottomSheet.kt | 15 ++ .../messagecomposer/MessageComposerEvents.kt | 1 + .../MessageComposerPresenter.kt | 10 + .../messagecomposer/MessageComposerState.kt | 1 + .../MessageComposerStateProvider.kt | 2 + .../messagecomposer/MessageComposerView.kt | 5 +- .../actionlist/ActionListPresenterTest.kt | 55 ++++ .../CreatePollEntryPoint.kt} | 18 +- features/poll/impl/build.gradle.kts | 5 +- .../features/poll/impl/PollFlowNode.kt | 70 ----- .../poll/impl/create/CreatePollEvents.kt | 31 +++ .../poll/impl/create/CreatePollNode.kt | 56 ++++ .../poll/impl/create/CreatePollPresenter.kt | 162 ++++++++++++ .../poll/impl/create/CreatePollState.kt | 35 +++ .../impl/create/CreatePollStateProvider.kt | 124 +++++++++ .../poll/impl/create/CreatePollView.kt | 187 ++++++++++++++ .../DefaultCreatePollEntryPoint.kt} | 24 +- .../impl/src/main/res/values/localazy.xml | 11 + .../impl/create/CreatePollPresenterTest.kt | 239 ++++++++++++++++++ .../libraries/featureflag/api/FeatureFlags.kt | 2 +- ...ePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png | 4 +- ...ePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png | 4 +- ...ePollView-D-0_1_null_0,NEXUS_5,1.0,en].png | 3 + ...ePollView-D-0_1_null_1,NEXUS_5,1.0,en].png | 3 + ...ePollView-D-0_1_null_2,NEXUS_5,1.0,en].png | 3 + ...ePollView-D-0_1_null_3,NEXUS_5,1.0,en].png | 3 + ...ePollView-D-0_1_null_4,NEXUS_5,1.0,en].png | 3 + ...ePollView-D-0_1_null_5,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_0,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_1,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_2,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_3,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_4,NEXUS_5,1.0,en].png | 3 + ...ePollView-N-0_2_null_5,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 6 + 40 files changed, 1032 insertions(+), 112 deletions(-) create mode 100644 changelog.d/1143.feature rename features/poll/api/src/main/kotlin/io/element/android/features/poll/api/{PollEntryPoint.kt => create/CreatePollEntryPoint.kt} (65%) delete mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt create mode 100644 features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt rename features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/{DefaultPollEntryPoint.kt => create/DefaultCreatePollEntryPoint.kt} (55%) create mode 100644 features/poll/impl/src/main/res/values/localazy.xml create mode 100644 features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png diff --git a/changelog.d/1143.feature b/changelog.d/1143.feature new file mode 100644 index 0000000000..84a86f4f25 --- /dev/null +++ b/changelog.d/1143.feature @@ -0,0 +1 @@ +Create poll. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index eee68768be..fcb2e7e5e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -46,6 +46,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode @@ -64,6 +65,7 @@ class MessagesFlowNode @AssistedInject constructor( @Assisted plugins: List, private val sendLocationEntryPoint: SendLocationEntryPoint, private val showLocationEntryPoint: ShowLocationEntryPoint, + private val createPollEntryPoint: CreatePollEntryPoint, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.Messages, @@ -101,6 +103,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data object SendLocation : NavTarget + + @Parcelize + data object CreatePoll : NavTarget } private val callback = plugins().firstOrNull() @@ -140,6 +145,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onSendLocationClicked() { backstack.push(NavTarget.SendLocation) } + + override fun onCreatePollClicked() { + backstack.push(NavTarget.CreatePoll) + } } createNode(buildContext, listOf(callback)) } @@ -179,6 +188,9 @@ class MessagesFlowNode @AssistedInject constructor( NavTarget.SendLocation -> { sendLocationEntryPoint.createNode(this, buildContext) } + NavTarget.CreatePoll -> { + createPollEntryPoint.createNode(this, buildContext) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 3f201a8e4c..6a3cf502d7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -58,6 +58,7 @@ class MessagesNode @AssistedInject constructor( fun onForwardEventClicked(eventId: EventId) fun onReportMessage(eventId: EventId, senderId: UserId) fun onSendLocationClicked() + fun onCreatePollClicked() } init { @@ -99,6 +100,10 @@ class MessagesNode @AssistedInject constructor( callback?.onSendLocationClicked() } + private fun onCreatePollClicked() { + callback?.onCreatePollClicked() + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -110,6 +115,7 @@ class MessagesNode @AssistedInject constructor( onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, onSendLocationClicked = this::onSendLocationClicked, + onCreatePollClicked = this::onCreatePollClicked, modifier = modifier, ) } 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 145813fe54..92b15f4fc0 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 @@ -97,6 +97,7 @@ fun MessagesView( onUserDataClicked: (UserId) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -175,6 +176,7 @@ fun MessagesView( onReactionLongClicked = ::onEmojiReactionLongClicked, onMoreReactionsClicked = ::onMoreReactionsClicked, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, onSwipeToReply = { targetEvent -> state.eventSink(MessagesEvents.HandleAction(TimelineItemAction.Reply, targetEvent)) }, @@ -267,6 +269,7 @@ private fun MessagesViewContent( onMessageLongClicked: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, ) { @@ -295,6 +298,7 @@ private fun MessagesViewContent( MessageComposerView( state = state.composerState, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, modifier = Modifier .fillMaxWidth() .wrapContentHeight(Alignment.Bottom) @@ -401,5 +405,6 @@ private fun ContentToPreview(state: MessagesState) { onPreviewAttachments = {}, onUserDataClicked = {}, onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index f71c750c22..e654365bcd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.features.messages.impl.timeline.model.event.canBeCopied @@ -96,6 +97,22 @@ class ActionListPresenter @Inject constructor( } } } + is TimelineItemPollContent -> { + buildList { + if (timelineItem.content.canBeCopied()) { + add(TimelineItemAction.Copy) + } + if (buildMeta.isDebuggable) { + add(TimelineItemAction.Developer) + } + if (!timelineItem.isMine) { + add(TimelineItemAction.ReportContent) + } + if (timelineItem.isMine || userCanRedact) { + add(TimelineItemAction.Redact) + } + } + } else -> buildList { if (timelineItem.isRemote) { // Can only reply or forward messages already uploaded to the server diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index b554ef98f4..43805bb5c0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -24,6 +24,7 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Collections import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.PhotoCamera @@ -52,6 +53,7 @@ import io.element.android.libraries.designsystem.theme.components.Text internal fun AttachmentsBottomSheet( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { val localView = LocalView.current @@ -85,6 +87,7 @@ internal fun AttachmentsBottomSheet( AttachmentSourcePickerMenu( state = state, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, ) } } @@ -95,6 +98,7 @@ internal fun AttachmentsBottomSheet( internal fun AttachmentSourcePickerMenu( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -131,6 +135,16 @@ internal fun AttachmentSourcePickerMenu( text = { Text(stringResource(R.string.screen_room_attachment_source_location)) }, ) } + if (state.canCreatePoll) { + ListItem( + modifier = Modifier.clickable { + state.eventSink(MessageComposerEvents.PickAttachmentSource.Poll) + onCreatePollClicked() + }, + icon = { Icon(Icons.Default.BarChart, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, + ) + } } } @@ -142,5 +156,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview { canShareLocation = true, ), onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index a39fe45ea8..d99eb3c158 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -35,6 +35,7 @@ sealed interface MessageComposerEvents { data object PhotoFromCamera : PickAttachmentSource data object VideoFromCamera : PickAttachmentSource data object Location : PickAttachmentSource + data object Poll : PickAttachmentSource } data object CancelSendAttachment : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 5477b10c63..cc735dc008 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -83,6 +83,11 @@ class MessageComposerPresenter @Inject constructor( canShareLocation.value = featureFlagService.isFeatureEnabled(FeatureFlags.LocationSharing) } + val canCreatePoll = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + canCreatePoll.value = featureFlagService.isFeatureEnabled(FeatureFlags.Polls) + } + val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> handlePickedMedia(attachmentsState, uri, mimeType) } @@ -179,6 +184,10 @@ class MessageComposerPresenter @Inject constructor( showAttachmentSourcePicker = false // Navigation to the location picker screen is done at the view layer } + MessageComposerEvents.PickAttachmentSource.Poll -> { + showAttachmentSourcePicker = false + // Navigation to the create poll screen is done at the view layer + } is MessageComposerEvents.CancelSendAttachment -> { ongoingSendAttachmentJob.value?.let { it.cancel() @@ -195,6 +204,7 @@ class MessageComposerPresenter @Inject constructor( mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, canShareLocation = canShareLocation.value, + canCreatePoll = canCreatePoll.value, attachmentsState = attachmentsState.value, eventSink = ::handleEvents ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 1b5bf3fe82..dbbc62ca47 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -29,6 +29,7 @@ data class MessageComposerState( val mode: MessageComposerMode, val showAttachmentSourcePicker: Boolean, val canShareLocation: Boolean, + val canCreatePoll: Boolean, val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index a1fbb7ffa0..2217b574b4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -33,6 +33,7 @@ fun aMessageComposerState( mode: MessageComposerMode = MessageComposerMode.Normal(content = ""), showAttachmentSourcePicker: Boolean = false, canShareLocation: Boolean = true, + canCreatePoll: Boolean = true, attachmentsState: AttachmentsState = AttachmentsState.None, ) = MessageComposerState( text = text, @@ -41,6 +42,7 @@ fun aMessageComposerState( mode = mode, showAttachmentSourcePicker = showAttachmentSourcePicker, canShareLocation = canShareLocation, + canCreatePoll = canCreatePoll, attachmentsState = attachmentsState, eventSink = {}, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 844635cef7..fd5421988b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.textcomposer.TextComposer fun MessageComposerView( state: MessageComposerState, onSendLocationClicked: () -> Unit, + onCreatePollClicked: () -> Unit, modifier: Modifier = Modifier, ) { fun onFullscreenToggle() { @@ -59,6 +60,7 @@ fun MessageComposerView( AttachmentsBottomSheet( state = state, onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, ) TextComposer( @@ -88,6 +90,7 @@ internal fun MessageComposerViewDarkPreview(@PreviewParameter(MessageComposerSta private fun ContentToPreview(state: MessageComposerState) { MessageComposerView( state = state, - onSendLocationClicked = {} + onSendLocationClicked = {}, + onCreatePollClicked = {}, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index afef9f6730..9ec1d087d0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -26,10 +26,14 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter 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.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent +import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.core.aBuildMeta import kotlinx.collections.immutable.persistentListOf @@ -369,6 +373,57 @@ class ActionListPresenterTest { assertThat(successState.displayEmojiReactions).isFalse() } } + + @Test + fun `present - compute for poll message`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = TimelineItemPollContent( + question = "Some question?", + answerItems = listOf( + PollAnswerItem( + answer = PollAnswer("id_1", "Answer1"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + PollAnswerItem( + answer = PollAnswer("id_2", "Answer2"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + ), + votes = mapOf(), + pollKind = PollKind.Disclosed, + isEnded = false, + ) + ) + + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.Redact, + ) + ) + ) + assertThat(successState.displayEmojiReactions).isTrue() + } + } } private fun anActionListPresenter(isBuildDebuggable: Boolean) = ActionListPresenter(buildMeta = aBuildMeta(isDebuggable = isBuildDebuggable)) diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt similarity index 65% rename from features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt rename to features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt index d8f2aed846..abbb041374 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollEntryPoint.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/create/CreatePollEntryPoint.kt @@ -14,24 +14,12 @@ * limitations under the License. */ -package io.element.android.features.poll.api +package io.element.android.features.poll.api.create import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint -interface PollEntryPoint : FeatureEntryPoint { - - fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder - - interface NodeBuilder { - fun callback(callback: Callback): NodeBuilder - fun build(): Node - } - - interface Callback : Plugin { - // Add your callbacks - } +interface CreatePollEntryPoint : FeatureEntryPoint { + fun createNode(parentNode: Node, buildContext: BuildContext): Node } - diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index 626a7d0f2c..4e8d36966a 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -14,8 +14,6 @@ * limitations under the License. */ -// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed -@Suppress("DSL_SCOPE_VIOLATION") plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) @@ -40,6 +38,8 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(projects.services.analytics.api) + implementation(projects.libraries.uiStrings) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.analytics.test) ksp(libs.showkase.processor) } diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt deleted file mode 100644 index f983025236..0000000000 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/PollFlowNode.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.poll.impl - -import android.os.Parcelable -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.bumble.appyx.core.composable.Children -import com.bumble.appyx.core.modality.BuildContext -import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin -import com.bumble.appyx.navmodel.backstack.BackStack -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.BackstackNode -import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler -import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.SessionScope -import kotlinx.parcelize.Parcelize - -@ContributesNode(SessionScope::class) -class PollFlowNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, -) : BackstackNode( - backstack = BackStack( - initialElement = NavTarget.Root, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins, -) { - - sealed interface NavTarget : Parcelable { - @Parcelize - data object Root : NavTarget - } - - override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { - return when (navTarget) { - NavTarget.Root -> { - createNode(buildContext) - } - } - } - - @Composable - override fun View(modifier: Modifier) { - Children( - navModel = backstack, - modifier = modifier, - transitionHandler = rememberDefaultTransitionHandler(), - ) - } -} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt new file mode 100644 index 0000000000..1251e07696 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollEvents.kt @@ -0,0 +1,31 @@ +/* + * 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.poll.impl.create + +import io.element.android.libraries.matrix.api.poll.PollKind + +sealed interface CreatePollEvents { + data object Create : CreatePollEvents + data class SetQuestion(val question: String) : CreatePollEvents + data class SetAnswer(val index: Int, val text: String) : CreatePollEvents + data object AddAnswer : CreatePollEvents + data class RemoveAnswer(val index: Int) : CreatePollEvents + data class SetPollKind(val pollKind: PollKind) : CreatePollEvents + data object NavBack : CreatePollEvents + data object ConfirmNavBack : CreatePollEvents + data object HideConfirmation : CreatePollEvents +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt new file mode 100644 index 0000000000..387f57a597 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollNode.kt @@ -0,0 +1,56 @@ +/* + * 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.poll.impl.create + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.RoomScope + +@ContributesNode(RoomScope::class) +class CreatePollNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: CreatePollPresenter.Factory, + // analyticsService: AnalyticsService, // TODO Polls: add analytics +) : Node(buildContext, plugins = plugins) { + + private val presenter = presenterFactory.create(backNavigator = ::navigateUp) + + init { + lifecycle.subscribe( + onResume = { + // TODO Polls: add analytics + // analyticsService.screen(MobileScreen(screenName = MobileScreen.ScreenName.PollView)) + } + ) + } + + @Composable + override fun View(modifier: Modifier) { + CreatePollView( + state = presenter.present(), + modifier = modifier, + ) + } +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt new file mode 100644 index 0000000000..b44afae9d1 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenter.kt @@ -0,0 +1,162 @@ +/* + * 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.poll.impl.create + +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.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch +import timber.log.Timber + +private const val MIN_ANSWERS = 2 +private const val MAX_ANSWERS = 20 +private const val MAX_ANSWER_LENGTH = 240 +private const val MAX_SELECTIONS = 1 + +class CreatePollPresenter @AssistedInject constructor( + private val room: MatrixRoom, + // private val analyticsService: AnalyticsService, // TODO Polls: add analytics + @Assisted private val navigateUp: () -> Unit, + // private val messageComposerContext: MessageComposerContext, // TODO Polls: add analytics +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(backNavigator: () -> Unit): CreatePollPresenter + } + + @Composable + override fun present(): CreatePollState { + + var question: String by rememberSaveable { mutableStateOf("") } + var answers: List by rememberSaveable() { mutableStateOf(listOf("", "")) } + var pollKind: PollKind by rememberSaveable(saver = pollKindSaver) { mutableStateOf(PollKind.Disclosed) } + var showConfirmation: Boolean by rememberSaveable { mutableStateOf(false) } + + val canCreate: Boolean by remember { derivedStateOf { canCreate(question, answers) } } + val canAddAnswer: Boolean by remember { derivedStateOf { canAddAnswer(answers) } } + val immutableAnswers: ImmutableList by remember { derivedStateOf { answers.toAnswers() } } + + val scope = rememberCoroutineScope() + + fun handleEvents(event: CreatePollEvents) { + when (event) { + is CreatePollEvents.Create -> scope.launch { + if (canCreate) { + room.createPoll( + question = question, + answers = answers, + maxSelections = MAX_SELECTIONS, + pollKind = pollKind, + ) + // analyticsService.capture(PollCreate()) // TODO Polls: add analytics + navigateUp() + } else { + Timber.d("Cannot create poll") + } + } + is CreatePollEvents.AddAnswer -> { + answers = answers + "" + } + is CreatePollEvents.RemoveAnswer -> { + answers = answers.filterIndexed { index, _ -> index != event.index } + } + is CreatePollEvents.SetAnswer -> { + answers = answers.toMutableList().apply { + this[event.index] = event.text.take(MAX_ANSWER_LENGTH) + } + } + is CreatePollEvents.SetPollKind -> { + pollKind = event.pollKind + } + is CreatePollEvents.SetQuestion -> { + question = event.question + } + is CreatePollEvents.NavBack -> { + navigateUp() + } + CreatePollEvents.ConfirmNavBack -> { + val shouldConfirm = question.isNotBlank() || answers.any { it.isNotBlank() } + if (shouldConfirm) { + showConfirmation = true + } else { + navigateUp() + } + } + is CreatePollEvents.HideConfirmation -> showConfirmation = false + } + } + + return CreatePollState( + canCreate = canCreate, + canAddAnswer = canAddAnswer, + question = question, + answers = immutableAnswers, + pollKind = pollKind, + showConfirmation = showConfirmation, + eventSink = ::handleEvents, + ) + } +} + +private fun canCreate( + question: String, + answers: List +) = question.isNotBlank() && answers.size >= MIN_ANSWERS && answers.all { it.isNotBlank() } + +private fun canAddAnswer(answers: List) = answers.size < MAX_ANSWERS + +private fun List.toAnswers(): ImmutableList { + return map { answer -> + Answer( + text = answer, + canDelete = this.size > MIN_ANSWERS, + ) + }.toImmutableList() +} + +private val pollKindSaver: Saver, Boolean> = Saver( + save = { + when (it.value) { + PollKind.Disclosed -> false + PollKind.Undisclosed -> true + } + }, + restore = { + mutableStateOf( + when(it) { + true -> PollKind.Undisclosed + else -> PollKind.Disclosed + } + ) + } +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt new file mode 100644 index 0000000000..eccaea45fc --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollState.kt @@ -0,0 +1,35 @@ +/* + * 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.poll.impl.create + +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.ImmutableList + +data class CreatePollState( + val canCreate: Boolean, + val canAddAnswer: Boolean, + val question: String, + val answers: ImmutableList, + val pollKind: PollKind, + val showConfirmation: Boolean, + val eventSink: (CreatePollEvents) -> Unit = {}, +) + +data class Answer( + val text: String, + val canDelete: Boolean, +) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt new file mode 100644 index 0000000000..29aa1288ad --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollStateProvider.kt @@ -0,0 +1,124 @@ +/* + * 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.poll.impl.create + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.poll.PollKind +import kotlinx.collections.immutable.persistentListOf + +class CreatePollStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + CreatePollState( + canCreate = false, + canAddAnswer = true, + question = "", + answers = persistentListOf( + Answer("", false), + Answer("", false) + ), + pollKind = PollKind.Disclosed, + showConfirmation = false, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", false), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", false), + ), + showConfirmation = true, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "What type of food should we have?", + answers = persistentListOf( + Answer("Italian \uD83C\uDDEE\uD83C\uDDF9", true), + Answer("Chinese \uD83C\uDDE8\uD83C\uDDF3", true), + Answer("Brazilian \uD83C\uDDE7\uD83C\uDDF7", true), + Answer("French \uD83C\uDDEB\uD83C\uDDF7", true), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = false, + question = "Should there be more than 20 answers?", + answers = persistentListOf( + Answer("1", true), + Answer("2", true), + Answer("3", true), + Answer("4", true), + Answer("5", true), + Answer("6", true), + Answer("7", true), + Answer("8", true), + Answer("9", true), + Answer("10", true), + Answer("11", true), + Answer("12", true), + Answer("13", true), + Answer("14", true), + Answer("15", true), + Answer("16", true), + Answer("17", true), + Answer("18", true), + Answer("19", true), + Answer("20", true), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ), + CreatePollState( + canCreate = true, + canAddAnswer = true, + question = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor" + + " in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt" + + " in culpa qui officia deserunt mollit anim id est laborum.", + answers = persistentListOf( + Answer( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + + " Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis a.", + false + ), + Answer( + "Laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore" + + " eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mol.", + false + ), + ), + showConfirmation = false, + pollKind = PollKind.Undisclosed, + ) + ) +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt new file mode 100644 index 0000000000..3e3ec4eb16 --- /dev/null +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -0,0 +1,187 @@ +/* + * 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.poll.impl.create + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.poll.impl.R +import io.element.android.libraries.designsystem.VectorIcons +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.HorizontalDivider +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconSource +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListItemStyle +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreatePollView( + state: CreatePollState, + modifier: Modifier = Modifier, +) { + val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } + BackHandler(onBack = navBack) + if (state.showConfirmation) ConfirmationDialog( + content = stringResource(id = R.string.screen_create_poll_confirmation), + onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, + onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } + ) + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.screen_create_poll_title), + style = ElementTheme.typography.aliasScreenTitle, + ) + }, + navigationIcon = { + BackButton(onClick = navBack) + }, + actions = { + TextButton( + text = stringResource(id = CommonStrings.action_create), + onClick = { state.eventSink(CreatePollEvents.Create) }, + enabled = state.canCreate, + ) + } + ) + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + .imePadding() + .fillMaxSize(), + ) { + item { + Text( + text = stringResource(id = R.string.screen_create_poll_question_desc), + modifier = Modifier.padding(start = 32.dp), + style = ElementTheme.typography.fontBodyMdRegular, + ) + } + item { + ListItem( + headlineContent = { + OutlinedTextField( + value = state.question, + onValueChange = { + state.eventSink(CreatePollEvents.SetQuestion(it)) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) + }, + ) + } + ) + } + itemsIndexed(state.answers) { index, answer -> + ListItem( + headlineContent = { + OutlinedTextField( + value = answer.text, + onValueChange = { + state.eventSink(CreatePollEvents.SetAnswer(index, it)) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1)) + }, + ) + }, + trailingContent = ListItemContent.Custom { + Icon( + resourceId = VectorIcons.Delete, + contentDescription = null, + modifier = Modifier.clickable(answer.canDelete) { + state.eventSink(CreatePollEvents.RemoveAnswer(index)) + }, + ) + }, + style = if (answer.canDelete) ListItemStyle.Destructive else ListItemStyle.Default, + ) + } + if (state.canAddAnswer) { + item { + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_add_option_btn)) }, + leadingContent = ListItemContent.Icon( + iconSource = IconSource.Vector(Icons.Default.Add), + ), + style = ListItemStyle.Primary, + onClick = { state.eventSink(CreatePollEvents.AddAnswer) }, + ) + } + } + item { + HorizontalDivider() + } + item { + ListItem( + headlineContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_headline)) }, + supportingContent = { Text(text = stringResource(id = R.string.screen_create_poll_anonymous_desc)) }, + trailingContent = ListItemContent.Switch( + checked = state.pollKind == PollKind.Undisclosed, + onChange = { state.eventSink(CreatePollEvents.SetPollKind(if (it) PollKind.Undisclosed else PollKind.Disclosed)) }, + ), + ) + } + } + } +} + +@DayNightPreviews +@Composable +internal fun CreatePollViewPreview( + @PreviewParameter(CreatePollStateProvider::class) state: CreatePollState +) = ElementPreview { + CreatePollView( + state = state, + ) +} diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt similarity index 55% rename from features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt rename to features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt index 052c1bcd5f..1ce64deb88 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/DefaultPollEntryPoint.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/DefaultCreatePollEntryPoint.kt @@ -14,33 +14,19 @@ * limitations under the License. */ -package io.element.android.features.poll.impl +package io.element.android.features.poll.impl.create import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node -import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.poll.api.PollEntryPoint +import io.element.android.features.poll.api.create.CreatePollEntryPoint import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.AppScope import javax.inject.Inject @ContributesBinding(AppScope::class) -class DefaultPollEntryPoint @Inject constructor() : PollEntryPoint { - - override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): PollEntryPoint.NodeBuilder { - val plugins = ArrayList() - - return object : PollEntryPoint.NodeBuilder { - - override fun callback(callback: PollEntryPoint.Callback): PollEntryPoint.NodeBuilder { - plugins += callback - return this - } - - override fun build(): Node { - return parentNode.createNode(buildContext, plugins) - } - } +class DefaultCreatePollEntryPoint @Inject constructor() : CreatePollEntryPoint { + override fun createNode(parentNode: Node, buildContext: BuildContext): Node { + return parentNode.createNode(buildContext) } } diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..846b247108 --- /dev/null +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -0,0 +1,11 @@ + + + "Add option" + "Show results only after poll ends" + "Anonymous Poll" + "Option %1$d" + "Are you sure you would like to go back?" + "Question or topic" + "What is the poll about?" + "Create Poll" + diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt new file mode 100644 index 0000000000..9dacc6062d --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -0,0 +1,239 @@ +/* + * 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.poll.impl.create + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.test.room.CreatePollInvocation +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CreatePollPresenterTest { + + private var navUpInvocationsCount = 0 + private val fakeMatrixRoom = FakeMatrixRoom() + // private val fakeAnalyticsService = FakeAnalyticsService() // TODO Polls: add analytics + + private val presenter = CreatePollPresenter( + room = fakeMatrixRoom, + // analyticsService = fakeAnalyticsService, // TODO Polls: add analytics + navigateUp = { navUpInvocationsCount++ }, + ) + + @Test + fun `default state has proper default values`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().let { + Truth.assertThat(it.canCreate).isEqualTo(false) + Truth.assertThat(it.canAddAnswer).isEqualTo(true) + Truth.assertThat(it.question).isEqualTo("") + Truth.assertThat(it.answers).isEqualTo(listOf(Answer("", false), Answer("", false))) + Truth.assertThat(it.pollKind).isEqualTo(PollKind.Disclosed) + Truth.assertThat(it.showConfirmation).isEqualTo(false) + } + } + } + + @Test + fun `non blank question and 2 answers are required to create a poll`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.canCreate).isEqualTo(false) + + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + val questionSet = awaitItem() + Truth.assertThat(questionSet.canCreate).isEqualTo(false) + + questionSet.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) + val answer1Set = awaitItem() + Truth.assertThat(answer1Set.canCreate).isEqualTo(false) + + answer1Set.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) + val answer2Set = awaitItem() + Truth.assertThat(answer2Set.canCreate).isEqualTo(true) + } + } + + @Test + fun `create polls sends a poll start event`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + initial.eventSink(CreatePollEvents.SetAnswer(0, "Answer 1")) + initial.eventSink(CreatePollEvents.SetAnswer(1, "Answer 2")) + skipItems(3) + initial.eventSink(CreatePollEvents.Create) + delay(1) // Wait for the coroutine to finish + Truth.assertThat(fakeMatrixRoom.createPollInvocations.size).isEqualTo(1) + Truth.assertThat(fakeMatrixRoom.createPollInvocations.last()).isEqualTo( + CreatePollInvocation( + question = "A question?", + answers = listOf("Answer 1", "Answer 2"), + maxSelections = 1, + pollKind = PollKind.Disclosed + ) + ) + } + } + + @Test + fun `add answer button adds an empty answer and removing it removes it`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.answers.size).isEqualTo(2) + + initial.eventSink(CreatePollEvents.AddAnswer) + val answerAdded = awaitItem() + Truth.assertThat(answerAdded.answers.size).isEqualTo(3) + Truth.assertThat(answerAdded.answers[2].text).isEqualTo("") + + initial.eventSink(CreatePollEvents.RemoveAnswer(2)) + val answerRemoved = awaitItem() + Truth.assertThat(answerRemoved.answers.size).isEqualTo(2) + } + } + + @Test + fun `set question sets the question`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("A question?")) + val questionSet = awaitItem() + Truth.assertThat(questionSet.question).isEqualTo("A question?") + } + } + + @Test + fun `set poll answer sets the given poll answer`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetAnswer(0, "This is answer 1")) + val answerSet = awaitItem() + Truth.assertThat(answerSet.answers.first().text).isEqualTo("This is answer 1") + } + } + + @Test + fun `set poll kind sets the poll kind`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetPollKind(PollKind.Undisclosed)) + val kindSet = awaitItem() + Truth.assertThat(kindSet.pollKind).isEqualTo(PollKind.Undisclosed) + } + } + + @Test + fun `can add options when between 2 and 20 and then no more`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.canAddAnswer).isEqualTo(true) + repeat(17) { + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(true) + } + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().canAddAnswer).isEqualTo(false) + } + } + + @Test + fun `can delete option if there are more than 2`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(initial.answers.all { it.canDelete }).isEqualTo(false) + initial.eventSink(CreatePollEvents.AddAnswer) + Truth.assertThat(awaitItem().answers.all { it.canDelete }).isEqualTo(true) + } + } + + @Test + fun `option with more than 240 char is truncated`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetAnswer(0, "A".repeat(241))) + Truth.assertThat(awaitItem().answers.first().text.length).isEqualTo(240) + } + } + + @Test + fun `navBack event calls navBack lambda`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + initial.eventSink(CreatePollEvents.NavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back with blank fields calls nav back lambda`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(initial.showConfirmation).isEqualTo(false) + initial.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(1) + } + } + + @Test + fun `confirm nav back with non blank fields shows confirmation dialog and sending hide hids it`() = runTest { + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initial = awaitItem() + initial.eventSink(CreatePollEvents.SetQuestion("Non blank")) + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + initial.eventSink(CreatePollEvents.ConfirmNavBack) + Truth.assertThat(navUpInvocationsCount).isEqualTo(0) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(true) + initial.eventSink(CreatePollEvents.HideConfirmation) + Truth.assertThat(awaitItem().showConfirmation).isEqualTo(false) + } + } +} diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 176bacb2c4..e1f4b740b0 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -29,7 +29,7 @@ enum class FeatureFlags( Polls( key = "feature.polls", title = "Polls", - description = "Render poll events in the timeline", + description = "Create poll and render poll events in the timeline", defaultValue = false, ) } diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png index 533e7086c5..58ebf07374 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04978db52b7be7d8aee3ac4aad1ec89ed4f8d9436fbd1829ec60c485e3fe8639 -size 21533 +oid sha256:79aeef6875265e119c3b4b97cea4d36ba3354ae52c4b94b69bbc09461b7bc319 +size 22259 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png index 7d03ec4b37..6e91b56f10 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52ce35020e0be63a86ad8b82f04b39e27b5960f7ae26a9ac5e1158884054608e -size 19859 +oid sha256:8dafa9a97ebc77f00fdb0432c7b94272f6ea1873c3475353be47ecde95e8b057 +size 20670 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f60c0c7a4d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b06ec4a259dfccec114689bff7d53089bb7fc64758af23372938fd83c422071 +size 35374 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c53d96b0e5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72bb304299954abac15f9487feab06649c0151c41cbcbcbf9c887417224d499b +size 39756 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc49158545 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:155cee63d3e031c912786b95bcc8e28dc4d1b296f8f0e4c01bd96398bb2dd040 +size 40623 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9de6f34f78 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb6afbfd69bf254a2d222deb72d80c7f0bb4fc44bc5010a7e34f2b82420a423 +size 47529 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..536ea963c9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2aca4289813d6dbce44fc19bc5e0f7f1dd9e670db4e31c5be93a9d0eac125fff +size 28696 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..29d5f43751 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-D-0_1_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2b68997a63739074bbb0622c2f96af9ad2309e61b4ac246a929d3d5e0134939 +size 124111 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ce95adf2e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f7fbf51ee1d86b1fc7cce949f88bfcb1c7ff7f700304e5a74242c4aa4965fcb +size 33455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8e11aa2691 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c7ca3daff99c6086f114d15dbf0649a4be7ff99a5afdeb6d0e8effda383bfec +size 36968 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..127289d30b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b682b0025c98d9d37ab8dc67cbc8a637f672ae2bf9e4a26e48209188d612c0c +size 36455 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..79faef2ce6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0680506590290c4ab5ae299abc51e0806b9a1e06a647971eae7b6a2227b0aba9 +size 44631 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..43d4dbb763 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b954906d2f4f0bb97f4ae78e246caab98a4d01efe04379ccf4ad79e8ae62310 +size 27091 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a8356b4a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.impl.create_null_CreatePollView-N-0_2_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:528e3f1f4fc9b8c236427882409bc718cd6565816ed137a47ba3dd2c69e103cc +size 108555 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 97acdf2ab5..4be78689de 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -120,6 +120,12 @@ "screen_welcome_.*", "screen_migration_.*" ] + }, + { + "name": ":features:poll:impl", + "includeRegex": [ + "screen_create_poll_.*" + ] } ] } From bb7553964761c455385ebfa545eff0131c863d94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 08:28:06 +0200 Subject: [PATCH 30/75] Update kotlin to 1.9.10 (#1123) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- build.gradle.kts | 2 +- features/login/impl/build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +++--- libraries/matrix/api/build.gradle.kts | 2 +- libraries/matrix/impl/build.gradle.kts | 2 +- libraries/push/impl/build.gradle.kts | 2 +- libraries/pushproviders/unifiedpush/build.gradle.kts | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 556ee5ff00..b48ef70ce7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ import org.jetbrains.kotlin.cli.common.toBooleanLenient buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") classpath("com.google.gms:google-services:4.3.15") } } diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 91b8a2f543..ae13197f05 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -19,7 +19,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) id("kotlin-parcelize") - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84bc4c1774..c8af411131 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,8 +4,8 @@ [versions] # Project android_gradle_plugin = "8.1.1" -kotlin = "1.9.0" -ksp = "1.9.0-1.0.13" +kotlin = "1.9.10" +ksp = "1.9.10-1.0.13" molecule = "1.2.0" # AndroidX @@ -23,7 +23,7 @@ browser = "1.6.0" # Compose compose_bom = "2023.08.00" -composecompiler = "1.5.1" +composecompiler = "1.5.3" # Coroutines coroutines = "1.7.3" diff --git a/libraries/matrix/api/build.gradle.kts b/libraries/matrix/api/build.gradle.kts index a1d508885a..1ffe07eb6f 100644 --- a/libraries/matrix/api/build.gradle.kts +++ b/libraries/matrix/api/build.gradle.kts @@ -18,7 +18,7 @@ plugins { id("io.element.android-library") id("kotlin-parcelize") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index e29d4b73db..b2a21f26df 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -17,7 +17,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 30a686cad6..b961146e78 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index a6565c25f0..28501b2fad 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -16,7 +16,7 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - kotlin("plugin.serialization") version "1.9.0" + kotlin("plugin.serialization") version "1.9.10" } android { From 0b88dac719de1227ea866957cf20e85e80318dfd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 30 Aug 2023 08:53:52 +0200 Subject: [PATCH 31/75] Fix test compilation --- .../features/messages/actionlist/ActionListPresenterTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index 9ec1d087d0..b3c805d32d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -405,7 +405,6 @@ class ActionListPresenterTest { percentage = 0.0f, ), ), - votes = mapOf(), pollKind = PollKind.Disclosed, isEnded = false, ) From c3f49a24519744e71d9a62c0a12774a10c6828fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Wed, 30 Aug 2023 09:33:53 +0200 Subject: [PATCH 32/75] Split link text into a `ListSupportingText` component. This also requires some internal changes to `ListSupportingText`, `ClickableLinkText` and `TimelineTextView` to match the behaviour and design. --- .../preferences/AnalyticsPreferencesView.kt | 65 ++++++++----------- .../components/event/TimelineItemTextView.kt | 53 ++++++++------- .../components/ClickableLinkText.kt | 4 +- .../theme/components/ListSection.kt | 17 +++-- 4 files changed, 69 insertions(+), 70 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 9920b8a0b0..2b70798cbd 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -16,26 +16,23 @@ package io.element.android.features.analytics.api.preferences -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.analytics.api.AnalyticsOptInEvents +import io.element.android.libraries.designsystem.components.LINK_TAG import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.ListSupportingText import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings -private const val LINK_TAG = "link" - @Composable fun AnalyticsPreferencesView( state: AnalyticsPreferencesState, @@ -46,43 +43,37 @@ fun AnalyticsPreferencesView( state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled)) } - val firstPart = stringResource(id = CommonStrings.screen_analytics_settings_help_us_improve, state.applicationName) - val secondPart = buildAnnotatedStringWithStyledPart( + val linkText = buildAnnotatedStringWithStyledPart( CommonStrings.screen_analytics_settings_read_terms, CommonStrings.screen_analytics_settings_read_terms_content_link, tagAndLink = LINK_TAG to state.policyUrl, ) - val subtitle = buildAnnotatedString { - append(firstPart) - append("\n\n") - append(secondPart) + val supportingText = stringResource( + id = CommonStrings.screen_analytics_settings_help_us_improve, + state.applicationName + ) + + Column(modifier) { + + ListItem( + headlineContent = { + Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data)) + }, + supportingContent = { + Text(supportingText) + }, + leadingContent = null, + trailingContent = ListItemContent.Switch( + checked = state.isEnabled, + ), + onClick = { + onEnabledChanged(!state.isEnabled) + } + ) + + ListSupportingText(annotatedString = linkText) } - ListItem( - headlineContent = { - Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data)) - }, - supportingContent = { - ClickableText( - text = subtitle, - onClick = { - subtitle - .getStringAnnotations(LINK_TAG, it, it) - .firstOrNull() - ?.let { stringAnnotation -> - onOpenAnalyticsPolicy(stringAnnotation.item) - } - }, - style = ElementTheme.typography.fontBodyMdRegular - .copy( - color = MaterialTheme.colorScheme.secondary, - ), - ) - }, - leadingContent = null, - trailingContent = ListItemContent.Switch(checked = state.isEnabled, onChange = ::onEnabledChanged), - modifier = modifier, - ) } @Preview diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt index 7b04d60ade..c6b0218bba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemTextView.kt @@ -21,7 +21,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +37,7 @@ import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.text.toAnnotatedString +import io.element.android.libraries.theme.ElementTheme @Composable fun TimelineItemTextView( @@ -45,31 +48,33 @@ fun TimelineItemTextView( onTextClicked: () -> Unit = {}, onTextLongClicked: () -> Unit = {}, ) { - val htmlDocument = content.htmlDocument - if (htmlDocument != null) { - // For now we ignore the extra padding for html content, so add some spacing - // below the content (as previous behavior) - Column(modifier = modifier) { - HtmlDocument( - document = htmlDocument, - modifier = Modifier, - onTextClicked = onTextClicked, - onTextLongClicked = onTextLongClicked, - interactionSource = interactionSource - ) - Spacer(Modifier.height(16.dp)) - } - } else { - Box(modifier) { - val textWithPadding = remember(content.body) { - content.body + extraPadding.getStr(16.sp).toAnnotatedString() + CompositionLocalProvider(LocalContentColor provides ElementTheme.colors.textPrimary) { + val htmlDocument = content.htmlDocument + if (htmlDocument != null) { + // For now we ignore the extra padding for html content, so add some spacing + // below the content (as previous behavior) + Column(modifier = modifier) { + HtmlDocument( + document = htmlDocument, + modifier = Modifier, + onTextClicked = onTextClicked, + onTextLongClicked = onTextLongClicked, + interactionSource = interactionSource + ) + Spacer(Modifier.height(16.dp)) + } + } else { + Box(modifier) { + val textWithPadding = remember(content.body) { + content.body + extraPadding.getStr(16.sp).toAnnotatedString() + } + ClickableLinkText( + text = textWithPadding, + onClick = onTextClicked, + onLongClick = onTextLongClicked, + interactionSource = interactionSource + ) } - ClickableLinkText( - text = textWithPadding, - onClick = onTextClicked, - onLongClick = onTextLongClicked, - interactionSource = interactionSource - ) } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index 39838a218a..7240ae4693 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -79,7 +78,7 @@ fun ClickableLinkText( @Composable fun ClickableLinkText( annotatedString: AnnotatedString, - interactionSource: MutableInteractionSource, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, modifier: Modifier = Modifier, linkify: Boolean = true, linkAnnotationTag: String = LINK_TAG, @@ -136,7 +135,6 @@ fun ClickableLinkText( layoutResult.value = it }, inlineContent = inlineContent, - color = MaterialTheme.colorScheme.primary, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt index ff58f1e2e0..cab1d493fc 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ListSection.kt @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle @@ -30,9 +29,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.ClickableLinkText import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup @@ -103,17 +104,21 @@ fun ListSupportingText( * @param modifier The modifier to be applied to the text. * @param contentPadding The padding to apply to the text. Default is [ListSupportingTextDefaults.Padding.Default]. */ +@OptIn(ExperimentalTextApi::class) @Composable fun ListSupportingText( annotatedString: AnnotatedString, modifier: Modifier = Modifier, contentPadding: ListSupportingTextDefaults.Padding = ListSupportingTextDefaults.Padding.Default, ) { - Text( - text = annotatedString, - modifier = modifier.padding(contentPadding.paddingValues()), - style = ElementTheme.typography.fontBodySmRegular, - color = ElementTheme.colors.textSecondary, + val style = ElementTheme.typography.fontBodySmRegular + .copy(color = ElementTheme.colors.textSecondary) + val paddedModifier = modifier.padding(contentPadding.paddingValues()) + ClickableLinkText( + annotatedString = annotatedString, + modifier = paddedModifier, + style = style, + linkify = false, ) } From 1c507862992b73733c1911163d41cfd7dc22b594 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Aug 2023 08:12:45 +0000 Subject: [PATCH 33/75] Update screenshots --- ..._AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ull_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png index 28ae52b035..48e393db15 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f70fad1a0ff08c3bd259ee8abd745d07474fa1dd45cfd2b8c77fe3536fedbb29 -size 23799 +oid sha256:b365229cac3351e4ec44979b7a22fcae090a8995e4784d9114cfd0033a242510 +size 23147 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png index 5ce51a0c7a..5790bbb52e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.analytics.api.preferences_null_AnalyticsPreferencesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07c00693ed47f8db94c0a9bcdd7350e32366813b3a4241dceebbd8aed78a5bbe -size 23880 +oid sha256:5fc43706603ce52fa3495fa4e6dfa9aa684540a9dec996a5f705427d34ffb55c +size 23274 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png index d12a018f64..98964b2b77 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcde2d058771af909595c4f163925fadc0ddee8cb0c440cbc3268403961e617c -size 25774 +oid sha256:e22dc131e8f1f7461c050e871eaa408895529976ef7445aa6faf09852370df90 +size 25176 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png index e86823967d..48fc8ae23e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.analytics_null_AnalyticsSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cef0f004854681538224bd5513e0ec66afb87e54fc7bac4a4eb4c0194c9f594c -size 26965 +oid sha256:bc234611dd3df74196467129476c69a0212a4c665f2a94797c175cbd911c2083 +size 26339 From 1b87565870397d6cbc2ed98c92067a86d38c16c1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 10:21:27 +0200 Subject: [PATCH 34/75] Remove unused lambda parameter. The link is opened by the LocalUriHandler now. --- .../api/preferences/AnalyticsPreferencesView.kt | 2 -- .../impl/analytics/AnalyticsSettingsNode.kt | 11 ----------- .../impl/analytics/AnalyticsSettingsView.kt | 3 --- 3 files changed, 16 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 2b70798cbd..468babcc6f 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -37,7 +37,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun AnalyticsPreferencesView( state: AnalyticsPreferencesState, modifier: Modifier = Modifier, - onOpenAnalyticsPolicy: (url: String) -> Unit, ) { fun onEnabledChanged(isEnabled: Boolean) { state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled)) @@ -90,6 +89,5 @@ internal fun AnalyticsPreferencesViewDarkPreview(@PreviewParameter(AnalyticsPref private fun ContentToPreview(state: AnalyticsPreferencesState) { AnalyticsPreferencesView( state = state, - onOpenAnalyticsPolicy = {}, ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt index 18bb99e8e1..adc917b7e6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsNode.kt @@ -16,19 +16,15 @@ package io.element.android.features.preferences.impl.analytics -import android.app.Activity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.theme.ElementTheme @ContributesNode(SessionScope::class) class AnalyticsSettingsNode @AssistedInject constructor( @@ -37,19 +33,12 @@ class AnalyticsSettingsNode @AssistedInject constructor( private val presenter: AnalyticsSettingsPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onOpenAnalyticsPolicy(activity: Activity, darkTheme: Boolean, url: String) { - activity.openUrlInChromeCustomTab(null, darkTheme, url) - } - @Composable override fun View(modifier: Modifier) { - val activity = LocalContext.current as Activity - val isDark = ElementTheme.colors.isLight.not() val state = presenter.present() AnalyticsSettingsView( state = state, onBackPressed = ::navigateUp, - onOpenAnalyticsPolicy = { onOpenAnalyticsPolicy(activity, darkTheme = isDark, it) }, modifier = modifier ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt index 83dd5554fd..3ee7365122 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/analytics/AnalyticsSettingsView.kt @@ -31,7 +31,6 @@ import io.element.android.libraries.ui.strings.CommonStrings fun AnalyticsSettingsView( state: AnalyticsSettingsState, onBackPressed: () -> Unit, - onOpenAnalyticsPolicy: (url: String) -> Unit, modifier: Modifier = Modifier, ) { PreferenceView( @@ -41,7 +40,6 @@ fun AnalyticsSettingsView( ) { AnalyticsPreferencesView( state = state.analyticsState, - onOpenAnalyticsPolicy = onOpenAnalyticsPolicy, ) } } @@ -61,6 +59,5 @@ private fun ContentToPreview(state: AnalyticsSettingsState) { AnalyticsSettingsView( state = state, onBackPressed = {}, - onOpenAnalyticsPolicy = {}, ) } From ed1c1b5048d2729cef61815e85cc8d02149d2515 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 10:24:57 +0200 Subject: [PATCH 35/75] Compact the code and reorder vals for code clarity --- .../api/preferences/AnalyticsPreferencesView.kt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt index 468babcc6f..b943199ba5 100644 --- a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/preferences/AnalyticsPreferencesView.kt @@ -42,18 +42,16 @@ fun AnalyticsPreferencesView( state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled)) } + val supportingText = stringResource( + id = CommonStrings.screen_analytics_settings_help_us_improve, + state.applicationName + ) val linkText = buildAnnotatedStringWithStyledPart( CommonStrings.screen_analytics_settings_read_terms, CommonStrings.screen_analytics_settings_read_terms_content_link, tagAndLink = LINK_TAG to state.policyUrl, ) - val supportingText = stringResource( - id = CommonStrings.screen_analytics_settings_help_us_improve, - state.applicationName - ) - Column(modifier) { - ListItem( headlineContent = { Text(stringResource(id = CommonStrings.screen_analytics_settings_share_data)) @@ -69,10 +67,8 @@ fun AnalyticsPreferencesView( onEnabledChanged(!state.isEnabled) } ) - ListSupportingText(annotatedString = linkText) } - } @Preview From 706896a4de30c88e2bf4c2ad1e70e4453f59dcb4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 10:28:44 +0200 Subject: [PATCH 36/75] Reorder params. --- .../libraries/designsystem/components/ClickableLinkText.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt index 7240ae4693..17e6eec258 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ClickableLinkText.kt @@ -78,8 +78,8 @@ fun ClickableLinkText( @Composable fun ClickableLinkText( annotatedString: AnnotatedString, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, linkify: Boolean = true, linkAnnotationTag: String = LINK_TAG, onClick: () -> Unit = {}, From 36f0bec184a6bd9985a30c3541dedc0c9d2b2ff6 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 30 Aug 2023 10:43:23 +0200 Subject: [PATCH 37/75] Make sure Snackbars are only displayed once (#1175) * Make sure Snackbars are only displayed once * Use a queue instead * Fix docs * Add tests for `SnackbarDispatcher`. --- changelog.d/928.bugfix | 1 + libraries/designsystem/build.gradle.kts | 6 ++ .../libraries/designsystem/utils/Snackbar.kt | 77 +++++++++++----- .../utils/SnackbarDispatcherTests.kt | 91 +++++++++++++++++++ 4 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 changelog.d/928.bugfix create mode 100644 libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt diff --git a/changelog.d/928.bugfix b/changelog.d/928.bugfix new file mode 100644 index 0000000000..98a4cd34e0 --- /dev/null +++ b/changelog.d/928.bugfix @@ -0,0 +1 @@ +Make sure Snackbars are only displayed once. diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 35b7a65cb4..f6eacf8746 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -43,5 +43,11 @@ android { ksp(libs.showkase.processor) kspTest(libs.showkase.processor) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt index c689d7e547..9458bf748a 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt @@ -34,33 +34,40 @@ import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.components.button.ButtonVisuals import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.Snackbar +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import java.util.concurrent.atomic.AtomicBoolean /** * A global dispatcher of [SnackbarMessage] to be displayed in [Snackbar] via a [SnackbarHostState]. */ class SnackbarDispatcher { - private val mutex = Mutex() - - private val _snackbarMessage = MutableStateFlow(null) - val snackbarMessage: Flow = _snackbarMessage.asStateFlow() - - suspend fun post(message: SnackbarMessage) { - mutex.withLock { - _snackbarMessage.update { message } + private val queueMutex = Mutex() + private val snackBarMessageQueue = ArrayDeque() + val snackbarMessage: Flow = flow { + while (currentCoroutineContext().isActive) { + queueMutex.lock() + emit(snackBarMessageQueue.firstOrNull()) } } - suspend fun clear() { - mutex.withLock { - _snackbarMessage.update { null } + suspend fun post(message: SnackbarMessage) { + if (snackBarMessageQueue.isEmpty()) { + snackBarMessageQueue.add(message) + if (queueMutex.isLocked) queueMutex.unlock() + } else { + snackBarMessageQueue.add(message) + } + } + + fun clear() { + if (snackBarMessageQueue.isNotEmpty()) { + snackBarMessageQueue.removeFirstOrNull() + if (queueMutex.isLocked) queueMutex.unlock() } } } @@ -87,31 +94,51 @@ fun SnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { } } +/** + * Helper method to display a [SnackbarMessage] in a [SnackbarHostState] handling cancellations. + */ @Composable fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostState { val snackbarHostState = remember { SnackbarHostState() } val snackbarMessageText = snackbarMessage?.let { stringResource(id = snackbarMessage.messageResId) - } + } ?: return snackbarHostState + val dispatcher = LocalSnackbarDispatcher.current - LaunchedEffect(snackbarMessage) { - if (snackbarMessageText == null) return@LaunchedEffect - launch { - snackbarHostState.showSnackbar( - message = snackbarMessageText, - duration = snackbarMessage.duration, - ) - if (isActive) { + LaunchedEffect(snackbarMessageText) { + // If the message wasn't already displayed, do it now, and mark it as displayed + // This will prevent the message from appearing in any other active SnackbarHosts + if (snackbarMessage.isDisplayed.getAndSet(true) == false) { + try { + snackbarHostState.showSnackbar( + message = snackbarMessageText, + duration = snackbarMessage.duration, + ) + // The snackbar item was displayed and dismissed, clear its message dispatcher.clear() + } catch (e: CancellationException) { + // The snackbar was being displayed when the coroutine was cancelled, + // so we need to clear its message + dispatcher.clear() + throw e } } } return snackbarHostState } +/** + * A message to be displayed in a [Snackbar]. + * @param messageResId The message to be displayed. + * @param duration The duration of the message. The default value is [SnackbarDuration.Short]. + * @param actionResId The action text to be displayed. The default value is `null`. + * @param isDisplayed Used to track if the current message is already displayed or not. + * @param action The action to be performed when the action is clicked. + */ data class SnackbarMessage( @StringRes val messageResId: Int, val duration: SnackbarDuration = SnackbarDuration.Short, @StringRes val actionResId: Int? = null, + val isDisplayed: AtomicBoolean = AtomicBoolean(false), val action: () -> Unit = {}, ) diff --git a/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt new file mode 100644 index 0000000000..3eb644d800 --- /dev/null +++ b/libraries/designsystem/src/test/kotlin/io/element/android/libraries/designsystem/utils/SnackbarDispatcherTests.kt @@ -0,0 +1,91 @@ +/* + * 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.libraries.designsystem.utils + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SnackbarDispatcherTests { + + @Test + fun `given an empty queue the flow emits a null item`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isNull() + } + } + + @Test + fun `given an empty queue calling clear does nothing`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + assertThat(awaitItem()).isNull() + snackbarDispatcher.clear() + expectNoEvents() + } + } + + @Test + fun `given a non-empty queue the flow emits an item`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + snackbarDispatcher.post(SnackbarMessage(0)) + val result = expectMostRecentItem() + assertThat(result).isNotNull() + } + } + + @Test + fun `given a call to clear, the current message is cleared`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + snackbarDispatcher.post(SnackbarMessage(0)) + val item = expectMostRecentItem() + assertThat(item).isNotNull() + snackbarDispatcher.clear() + assertThat(awaitItem()).isNull() + } + } + + @Test + fun `given 2 message emissions, the next message is displayed only after a call to clear`() = runTest { + val snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher.snackbarMessage.test { + val messageA = SnackbarMessage(0) + val messageB = SnackbarMessage(1) + + // Send message A - it is the most recent item + snackbarDispatcher.post(messageA) + assertThat(expectMostRecentItem()).isEqualTo(messageA) + + // Send message B - message A is still the most recent item + snackbarDispatcher.post(messageB) + expectNoEvents() + + // Clear the last message - message B is now the most recent item + snackbarDispatcher.clear() + assertThat(expectMostRecentItem()).isEqualTo(messageB) + + // Clear again - the queue is empty + snackbarDispatcher.clear() + assertThat(awaitItem()).isNull() + } + } + +} From 7129a526b073fae55a061ba9102df5c4dd0efe84 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 12:48:33 +0200 Subject: [PATCH 38/75] Iterate and reformulate after PR review. --- docs/install_from_github_release.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/install_from_github_release.md b/docs/install_from_github_release.md index eec5586d82..57363a617c 100644 --- a/docs/install_from_github_release.md +++ b/docs/install_from_github_release.md @@ -1,4 +1,4 @@ -# Installing Element X Android from a Github Release. +# Installing Element X Android from a Github Release This document explains how to install Element X Android from a Github Release. @@ -12,9 +12,18 @@ This document explains how to install Element X Android from a Github Release. ## Requirements -The Github release will contain an Android App Bundle (with `aab` extension) file, and not APKs like on the Element Android project. So there is some steps to perform to generate and signed the APKs from the App Bundle. The generated APKs will then be selected depending on your phone configuration to be installed on a device (emulator or real device). +The Github release will contain an Android App Bundle (with `aab` extension) file, unlike in the Element Android project where releases directly provide the APKs. So there are some steps to perform to generate and sign App Bundle APKs. An APK suitable for the targeted device will then be generated. -The easiest way to do that is to use the debug signature that is shared between the developers and stored in the project. So we recommend to checkout the project first. Android Studio is not required, you can use the command line. +The easiest way to do that is to use the debug signature that is shared between the developers and stored in the Element X Android project. So we recommend to clone the project first, to be able to use the debug signature it contains. But note that you can use any other signature. You don't need to install Android Studio, you will only need a shell terminal. + +You can clone the project by running: +```bash +git clone git@github.com:vector-im/element-x-android.git +``` +or +```bash +git clone https://github.com/vector-im/element-x-android.gi +``` You will also need to install [bundletool](https://developer.android.com/studio/command-line/bundletool). On MacOS, you can run the following command: @@ -24,7 +33,7 @@ brew install bundletool ## Steps -1. Open the GitHub release that you want to install from https://github.com/vector-im/element-x-android/releases; +1. Open the GitHub release that you want to install from https://github.com/vector-im/element-x-android/releases 2. Download the asset `app-release-signed.aab` 3. Navigate to the folder where you cloned the project and run the following command: ```bash @@ -38,7 +47,7 @@ bundletool build-apks --bundle=./tmp/Element/0.1.5/app-release-signed.aab --outp --ks=./app/signature/debug.keystore --ks-pass=pass:android --ks-key-alias=androiddebugkey --key-pass=pass:android \ --overwrite ``` -4. Run an Android emulator, or connect a real device to your computer. +4. Run an Android emulator, or connect a real device to your computer 5. Install the APKs on the device: ```bash bundletool install-apks --apks=./tmp/elementx.apks @@ -50,6 +59,6 @@ That's it, the application should be installed on your device, you can start it If the application was already installed on your phone, there are several cases: -- it was installed from the PlayStore, you will have to uninstall it first, because the signature will not match. +- it was installed from the PlayStore, you will have to uninstall it first because the signature will not match. - it was installed from a previous GitHub release, this is like an application upgrade, so no need to uninstall the existing app. - it was installed from a more recent GitHub release, you will have to uninstall it first. From 4a88e3fab68424433abe4fff6d3d11812d469a3a Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 30 Aug 2023 12:49:58 +0200 Subject: [PATCH 39/75] Bug reporter crashes when 'send logs' is disabled. (#1184) * Bug reporter crashes when 'send logs' is disabled. * Make sure generated files are cleaned up when uploading the logs fails. --- changelog.d/1168.bugfix | 1 + .../rageshake/impl/reporter/DefaultBugReporter.kt | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1168.bugfix diff --git a/changelog.d/1168.bugfix b/changelog.d/1168.bugfix new file mode 100644 index 0000000000..f7f959ac0a --- /dev/null +++ b/changelog.d/1168.bugfix @@ -0,0 +1 @@ +Bug reporter crashes when 'send logs' is disabled. diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index ad7f58a239..46ccc7fc56 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -268,12 +268,13 @@ class DefaultBugReporter @Inject constructor( } } - if (!uploadedSomeLogs) { - error("Couldn't upload any logs") - } - mBugReportFiles.addAll(gzippedFiles) + if (gzippedFiles.isNotEmpty() && !uploadedSomeLogs) { + serverError = "Couldn't upload any logs, please retry." + return@withContext + } + if (withScreenshot) { screenshotHolder.getFileUri() ?.toUri() From a5d42f61faca024a206ffddeb8fc36e81b50423a Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 30 Aug 2023 12:47:31 +0100 Subject: [PATCH 40/75] 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()) + } +} From 10060c4476695f3c0835a224ef5c7c088d143f17 Mon Sep 17 00:00:00 2001 From: David Langley Date: Wed, 30 Aug 2023 12:55:50 +0100 Subject: [PATCH 41/75] lint --- .../components/customreaction/CustomReactionBottomSheet.kt | 1 - .../components/customreaction/CustomReactionPresenter.kt | 7 ------- .../components/customreaction/CustomReactionState.kt | 5 ----- 3 files changed, 13 deletions(-) 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 ed78993716..3fe739a592 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,7 +23,6 @@ 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 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 8a331438a5..6831ca7dfa 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 @@ -18,19 +18,12 @@ package io.element.android.features.messages.impl.timeline.components.customreac 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 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 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 400c34d6fa..f6f7d2b0f9 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,12 +17,7 @@ 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( From 81f7cd5c60426f2e8b4307a10f3eda380957bb31 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 30 Aug 2023 12:38:04 +0000 Subject: [PATCH 42/75] Update screenshots --- ...tomreaction_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png | 3 +++ ...omreaction_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png | 3 +++ ....components_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png | 3 --- ...components_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png | 3 --- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..aaab9d35c1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ea42394330d37a0aa5ba8940e023fc4454d902caf6d265adec0e11c8a34d85f +size 187744 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..65a27f228d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components.customreaction_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fb074cd58034c90010dc437fc01d6ea77a2a22aa07bc1d4cf0c1730b543b41a +size 186707 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 8838c1cbf2..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerDark_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd761ca4ff5a70770869780c2575006ed8bd187ed4b3f197ffe1e4ea18d8390b -size 189559 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 80a5da9cd1..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.timeline.components_null_EmojiPickerLight_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8c6e3f139ca634c47c45fbeeaccf3e7e903fb8f83a88fdf8d7247455e76e4a9 -size 188653 From b8d6db2f147d4c37c02d96d4a3f9e24085ee8889 Mon Sep 17 00:00:00 2001 From: axel simon Date: Wed, 30 Aug 2023 13:39:05 +0100 Subject: [PATCH 43/75] Update README.md (#1187) Remove unnecessary year from copyright mention (it was already out of date). Cf. https://hynek.me/til/copyright-years/ Signed-off-by: axel simon --- README.md | 2 +- changelog.d/1187.misc | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1187.misc diff --git a/README.md b/README.md index f24efcd828..2a0d352187 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ If after your research you still have a question, ask at [#element-x-android:mat ## Copyright & License -Copyright (c) 2022 New Vector Ltd +Copyright © New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the [LICENSE](LICENSE) file, or at: diff --git a/changelog.d/1187.misc b/changelog.d/1187.misc new file mode 100644 index 0000000000..301e3a6fc4 --- /dev/null +++ b/changelog.d/1187.misc @@ -0,0 +1 @@ +Remove unnecessary year in copyright mention. From 51bb7febd645fbf9f9a4a9401747d74c5824c901 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 30 Aug 2023 16:31:37 +0200 Subject: [PATCH 44/75] Upgrade rust sdk to v48 (#1186) - Sends content instead of string in message reply and edit - Adds poll response and end APIs - Adds logoUri to OidcConfiguration --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/api/room/MatrixRoom.kt | 16 +++++++ .../libraries/matrix/impl/auth/OidcConfig.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 32 ++++++++++++-- .../matrix/test/room/FakeMatrixRoom.kt | 42 +++++++++++++++++++ 5 files changed, 88 insertions(+), 5 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c8af411131..0d52d1e0df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -146,7 +146,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.47" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.48" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 88d35c83d4..41f105df8e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -157,6 +157,22 @@ interface MatrixRoom : Closeable { pollKind: PollKind, ): Result + /** + * Send a response to a poll. + * + * @param pollStartId The event ID of the poll start event. + * @param answers The list of answer ids to send. + */ + suspend fun sendPollResponse(pollStartId: EventId, answers: List): Result + + /** + * Ends a poll in the room. + * + * @param pollStartId The event ID of the poll start event. + * @param text Fallback text of the poll end event. + */ + suspend fun endPoll(pollStartId: EventId, text: String): Result + override fun close() = destroy() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt index 401fa0ce83..dae032f516 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt @@ -23,6 +23,7 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration( clientName = "Element", redirectUri = OidcConfig.redirectUri, clientUri = "https://element.io", + logoUri = "https://element.io/mobile-icon.png", tosUri = "https://element.io/user-terms-of-service", policyUri = "https://element.io/privacy", /** diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index ffe06379d3..71caed8f19 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -215,7 +215,7 @@ class RustMatrixRoom( override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, message: String): Result = withContext(roomDispatcher) { if (originalEventId != null) { runCatching { - innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId?.value) + innerRoom.edit(messageEventContentFromMarkdown(message), originalEventId.value, transactionId?.value) } } else { runCatching { @@ -226,10 +226,8 @@ class RustMatrixRoom( } override suspend fun replyMessage(eventId: EventId, message: String): Result = withContext(roomDispatcher) { - val transactionId = genTransactionId() - // val content = messageEventContentFromMarkdown(message) runCatching { - innerRoom.sendReply(/* TODO use content */ message, eventId.value, transactionId) + innerRoom.sendReply(messageEventContentFromMarkdown(message), eventId.value, genTransactionId()) } } @@ -402,6 +400,32 @@ class RustMatrixRoom( } } + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.sendPollResponse( + pollStartId = pollStartId.value, + answers = answers, + txnId = genTransactionId(), + ) + } + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = withContext(roomDispatcher) { + runCatching { + innerRoom.endPoll( + pollStartId = pollStartId.value, + text = text, + txnId = genTransactionId(), + ) + } + } + private suspend fun sendAttachment(files: List, handle: () -> SendAttachmentJoinHandle): Result { return runCatching { MediaUploadHandlerImpl(files, handle()) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 88f705162e..7bffb985bb 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -85,6 +85,8 @@ class FakeMatrixRoom( private var reportContentResult = Result.success(Unit) private var sendLocationResult = Result.success(Unit) private var createPollResult = Result.success(Unit) + private var sendPollResponseResult = Result.success(Unit) + private var endPollResult = Result.success(Unit) private var progressCallbackValues = emptyList>() val editMessageCalls = mutableListOf() @@ -109,6 +111,12 @@ class FakeMatrixRoom( private val _createPollInvocations = mutableListOf() val createPollInvocations: List = _createPollInvocations + private val _sendPollResponseInvocations = mutableListOf() + val sendPollResponseInvocations: List = _sendPollResponseInvocations + + private val _endPollInvocations = mutableListOf() + val endPollInvocations: List = _endPollInvocations + var invitedUserId: UserId? = null private set @@ -320,6 +328,22 @@ class FakeMatrixRoom( return createPollResult } + override suspend fun sendPollResponse( + pollStartId: EventId, + answers: List + ): Result = simulateLongTask { + _sendPollResponseInvocations.add(SendPollResponseInvocation(pollStartId, answers)) + return sendPollResponseResult + } + + override suspend fun endPoll( + pollStartId: EventId, + text: String + ): Result = simulateLongTask { + _endPollInvocations.add(EndPollInvocation(pollStartId, text)) + return endPollResult + } + fun givenLeaveRoomError(throwable: Throwable?) { this.leaveRoomError = throwable } @@ -416,6 +440,14 @@ class FakeMatrixRoom( createPollResult = result } + fun givenSendPollResponseResult(result: Result) { + sendPollResponseResult = result + } + + fun givenEndPollResult(result: Result) { + endPollResult = result + } + fun givenProgressCallbackValues(values: List>) { progressCallbackValues = values } @@ -435,3 +467,13 @@ data class CreatePollInvocation( val maxSelections: Int, val pollKind: PollKind, ) + +data class SendPollResponseInvocation( + val pollStartId: EventId, + val answers: List, +) + +data class EndPollInvocation( + val pollStartId: EventId, + val text: String, +) From df5643bc1f95d8196ee11228209548fcead6f161 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 16:47:40 +0200 Subject: [PATCH 45/75] Rename file and update `tosUri` value. --- .../matrix/impl/auth/{OidcConfig.kt => OidcConfiguration.kt} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/{OidcConfig.kt => OidcConfiguration.kt} (95%) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt similarity index 95% rename from libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt rename to libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt index dae032f516..f49ee65208 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfig.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/OidcConfiguration.kt @@ -24,7 +24,7 @@ val oidcConfiguration: OidcConfiguration = OidcConfiguration( redirectUri = OidcConfig.redirectUri, clientUri = "https://element.io", logoUri = "https://element.io/mobile-icon.png", - tosUri = "https://element.io/user-terms-of-service", + tosUri = "https://element.io/acceptable-use-policy-terms", policyUri = "https://element.io/privacy", /** * Some homeservers/auth issuers don't support dynamic client registration, and have to be registered manually From d46b649a7309f6d7d096b0c9d949980a6bfaa1c1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 30 Aug 2023 16:59:42 +0200 Subject: [PATCH 46/75] We are now using kotlinc 1.9.10, so Android Studio is updating this file. --- .idea/kotlinc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d994a6..f8467b458e 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file From f0bed854588a57d8456cfd7f5703ce84409e4852 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 30 Aug 2023 17:05:11 +0200 Subject: [PATCH 47/75] Poll vote (#1181) - Adds sendPollVote rust room API (still not operational, need to wait for a rust sdk release) - Adds an optional EventId in TimelineItemPollContent - Wires the poll answer click listeners all the way to the TimelinePresenter in order to call the new room API - Shows question as message summary in long press menu Closes https://github.com/vector-im/element-meta/issues/2025 --- .../messages/impl/timeline/TimelineEvents.kt | 4 +++ .../impl/timeline/TimelinePresenter.kt | 7 ++++ .../messages/impl/timeline/TimelineView.kt | 7 ++++ .../components/TimelineItemEventRow.kt | 17 ++++++++- .../components/TimelineItemStateEventRow.kt | 1 + .../event/TimelineItemContentView.kt | 4 ++- .../components/event/TimelineItemPollView.kt | 7 ++-- .../event/TimelineItemContentFactory.kt | 2 +- .../event/TimelineItemContentPollFactory.kt | 7 +++- .../model/event/TimelineItemPollContent.kt | 2 ++ .../event/TimelineItemPollContentProvider.kt | 2 ++ .../MessageSummaryFormatterImpl.kt | 2 +- .../actionlist/ActionListPresenterTest.kt | 2 ++ .../timeline/TimelinePresenterTest.kt | 32 ++++++++++++++++- .../TimelineItemContentPollFactoryTest.kt | 35 +++++++++++++------ .../features/poll/api/PollContentView.kt | 19 +++++++--- 16 files changed, 125 insertions(+), 25 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 30f9aade79..e427646a71 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -22,4 +22,8 @@ sealed interface TimelineEvents { data object LoadMore : TimelineEvents data class SetHighlightedEvent(val eventId: EventId?) : TimelineEvents data class OnScrollFinished(val firstIndex: Int) : TimelineEvents + data class PollAnswerSelected( + val pollStartId: EventId, + val answerId: String + ) : TimelineEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index c53d8fc279..402c332855 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -87,6 +87,13 @@ class TimelinePresenter @Inject constructor( lastReadReceiptId = lastReadReceiptId ) } + is TimelineEvents.PollAnswerSelected -> appScope.launch { + room.sendPollResponse( + pollStartId = event.pollStartId, + answers = listOf(event.answerId), + ) + // TODO Polls: Send poll vote analytic + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 6e16a3b92d..ff90e8d29a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -100,6 +100,9 @@ fun TimelineView( // TODO implement this logic once we have support to 'jump to event X' in sliding sync } + fun onPollAnswerSelected(pollStartId: EventId, answerId: String) { + state.eventSink(TimelineEvents.PollAnswerSelected(pollStartId, answerId)) + } Box(modifier = modifier) { LazyColumn( @@ -125,6 +128,7 @@ fun TimelineView( onReactionLongClick = onReactionLongClicked, onMoreReactionsClick = onMoreReactionsClicked, onTimestampClicked = onTimestampClicked, + onPollAnswerSelected = ::onPollAnswerSelected, onSwipeToReply = onSwipeToReply, ) } @@ -162,6 +166,7 @@ fun TimelineItemRow( onMoreReactionsClick: (TimelineItem.Event) -> Unit, onTimestampClicked: (TimelineItem.Event) -> Unit, onSwipeToReply: (TimelineItem.Event) -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { when (timelineItem) { @@ -194,6 +199,7 @@ fun TimelineItemRow( onMoreReactionsClick = onMoreReactionsClick, onTimestampClicked = onTimestampClicked, onSwipeToReply = { onSwipeToReply(timelineItem) }, + onPollAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } @@ -231,6 +237,7 @@ fun TimelineItemRow( onReactionClick = onReactionClick, onReactionLongClick = onReactionLongClick, onMoreReactionsClick = onMoreReactionsClick, + onPollAnswerSelected = onPollAnswerSelected, onSwipeToReply = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index dd5a2df2e0..10671ee00f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -118,6 +118,7 @@ fun TimelineItemEventRow( onReactionLongClick: (emoji: String, eventId: TimelineItem.Event) -> Unit, onMoreReactionsClick: (eventId: TimelineItem.Event) -> Unit, onSwipeToReply: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { val coroutineScope = rememberCoroutineScope() @@ -175,6 +176,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, + onPollAnswerSelected = onPollAnswerSelected, ) } } @@ -191,6 +193,7 @@ fun TimelineItemEventRow( onReactionClicked = { emoji -> onReactionClick(emoji, event) }, onReactionLongClicked = { emoji -> onReactionLongClick(emoji, event) }, onMoreReactionsClicked = { onMoreReactionsClick(event) }, + onPollAnswerSelected = onPollAnswerSelected, ) } } @@ -232,6 +235,7 @@ private fun TimelineItemEventRowContent( onReactionClicked: (emoji: String) -> Unit, onReactionLongClicked: (emoji: String) -> Unit, onMoreReactionsClicked: (event: TimelineItem.Event) -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { fun ConstrainScope.linkStartOrEnd(event: TimelineItem.Event) = if (event.isMine) { @@ -289,7 +293,8 @@ private fun TimelineItemEventRowContent( inReplyToClick = inReplyToClicked, onTimestampClicked = { onTimestampClicked(event) - } + }, + onPollAnswerSelected = onPollAnswerSelected, ) } @@ -360,6 +365,7 @@ private fun MessageEventBubbleContent( onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, onTimestampClicked: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, @SuppressLint("ModifierParameter") bubbleModifier: Modifier = Modifier, // need to rename this modifier to distinguish it from the following ones ) { val timestampPosition = when (event.content) { @@ -385,6 +391,7 @@ private fun MessageEventBubbleContent( onClick = onMessageClick, onLongClick = onMessageLongClick, extraPadding = event.toExtraPadding(), + onPollAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } @@ -607,6 +614,7 @@ private fun ContentToPreview() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -627,6 +635,7 @@ private fun ContentToPreview() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -674,6 +683,7 @@ private fun ContentToPreviewWithReply() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) TimelineItemEventRow( event = aTimelineItemEvent( @@ -695,6 +705,7 @@ private fun ContentToPreviewWithReply() { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -752,6 +763,7 @@ private fun ContentTimestampToPreview(event: TimelineItem.Event) { onMoreReactionsClick = {}, onTimestampClicked = {}, onSwipeToReply = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -792,6 +804,7 @@ private fun ContentWithManyReactionsToPreview() { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } } @@ -816,6 +829,7 @@ internal fun TimelineItemEventRowLongSenderNamePreview() = ElementPreviewLight { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } @@ -836,5 +850,6 @@ internal fun TimelineItemEventTimestampBelowPreview() = ElementPreviewLight { onMoreReactionsClick = {}, onSwipeToReply = {}, onTimestampClicked = {}, + onPollAnswerSelected = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt index d22182d098..b976d24a25 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemStateEventRow.kt @@ -70,6 +70,7 @@ fun TimelineItemStateEventRow( onClick = onClick, onLongClick = onLongClick, extraPadding = noExtraPadding, + onPollAnswerSelected = { _, _ -> error("Polls are not supported in state events") }, modifier = Modifier.defaultTimelineContentPadding() ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index d53e3f1e5b..e882950d6c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -31,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.libraries.matrix.api.core.EventId @Composable fun TimelineItemEventContentView( @@ -39,6 +40,7 @@ fun TimelineItemEventContentView( extraPadding: ExtraPadding, onClick: () -> Unit, onLongClick: () -> Unit, + onPollAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier ) { when (content) { @@ -93,7 +95,7 @@ fun TimelineItemEventContentView( ) is TimelineItemPollContent -> TimelineItemPollView( content = content, - onAnswerSelected = {}, + onAnswerSelected = onPollAnswerSelected, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt index 3608593cde..ec784ad331 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemPollView.kt @@ -24,16 +24,17 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.PollContentView import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview -import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.core.EventId import kotlinx.collections.immutable.toImmutableList @Composable fun TimelineItemPollView( content: TimelineItemPollContent, - onAnswerSelected: (PollAnswer) -> Unit, + onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { PollContentView( + eventId = content.eventId, question = content.question, answerItems = content.answerItems.toImmutableList(), pollKind = content.pollKind, @@ -49,6 +50,6 @@ internal fun TimelineItemPollViewPreview(@PreviewParameter(TimelineItemPollConte ElementPreview { TimelineItemPollView( content = content, - onAnswerSelected = {}, + onAnswerSelected = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt index 1de4ee7c86..6a74fd11a5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentFactory.kt @@ -55,7 +55,7 @@ class TimelineItemContentFactory @Inject constructor( is RoomMembershipContent -> roomMembershipFactory.create(eventTimelineItem) is StateContent -> stateFactory.create(eventTimelineItem) is StickerContent -> stickerFactory.create(itemContent) - is PollContent -> pollFactory.create(itemContent) + is PollContent -> pollFactory.create(itemContent, eventTimelineItem.eventId) is UnableToDecryptContent -> utdFactory.create(itemContent) is UnknownContent -> TimelineItemUnknownContent } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt index 9c06b17056..04551f7086 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactory.kt @@ -23,6 +23,7 @@ import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.isDisclosed import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import javax.inject.Inject @@ -32,7 +33,10 @@ class TimelineItemContentPollFactory @Inject constructor( private val featureFlagService: FeatureFlagService, ) { - suspend fun create(content: PollContent): TimelineItemEventContent { + suspend fun create( + content: PollContent, + eventId: EventId? + ): TimelineItemEventContent { if (!featureFlagService.isFeatureEnabled(FeatureFlags.Polls)) return TimelineItemUnknownContent // Todo Move this computation to the matrix rust sdk @@ -67,6 +71,7 @@ class TimelineItemContentPollFactory @Inject constructor( } return TimelineItemPollContent( + eventId = eventId, question = content.question, answerItems = answerItems, pollKind = content.kind, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt index 0f94b97776..dc47c12a86 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContent.kt @@ -17,9 +17,11 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind data class TimelineItemPollContent( + val eventId: EventId?, val question: String, val answerItems: List, val pollKind: PollKind, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt index 247d450ae7..c475f6dfbd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemPollContentProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.poll.api.aPollAnswerItemList +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind open class TimelineItemPollContentProvider : PreviewParameterProvider { @@ -30,6 +31,7 @@ open class TimelineItemPollContentProvider : PreviewParameterProvider context.getString(CommonStrings.common_shared_location) is TimelineItemEncryptedContent -> context.getString(CommonStrings.common_unable_to_decrypt) is TimelineItemRedactedContent -> context.getString(CommonStrings.common_message_removed) - is TimelineItemPollContent, // Todo Polls: handle summary + is TimelineItemPollContent -> event.content.question is TimelineItemUnknownContent -> context.getString(CommonStrings.common_unsupported_event) is TimelineItemImageContent -> context.getString(CommonStrings.common_image) is TimelineItemVideoContent -> context.getString(CommonStrings.common_video) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index b3c805d32d..111b3a370d 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.A_MESSAGE @@ -384,6 +385,7 @@ class ActionListPresenterTest { val messageEvent = aMessageEvent( isMine = true, content = TimelineItemPollContent( + eventId = EventId("\$anEventId"), question = "Some question?", answerItems = listOf( PollAnswerItem( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index b4bb14f672..860f8def37 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -25,7 +25,7 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem -import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction @@ -36,8 +36,10 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.tests.testutils.awaitWithLatch import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -248,6 +250,23 @@ class TimelinePresenterTest { } } + @Test + fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest { + val room = FakeMatrixRoom() + val presenter = createTimelinePresenter(room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId")) + } + delay(1) + assertThat(room.sendPollResponseInvocations.size).isEqualTo(1) + assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId")) + assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) + // TODO Polls: Test poll vote analytic + } + private fun TestScope.createTimelinePresenter( timeline: MatrixTimeline = FakeMatrixTimeline(), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory() @@ -259,4 +278,15 @@ class TimelinePresenterTest { appScope = this ) } + + private fun TestScope.createTimelinePresenter( + room: MatrixRoom, + ): TimelinePresenter { + return TimelinePresenter( + timelineItemsFactory = aTimelineItemsFactory(), + room = room, + dispatchers = testCoroutineDispatchers(), + appScope = this + ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt index a1411293e9..8cf5704bba 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/factories/event/TimelineItemContentPollFactoryTest.kt @@ -22,10 +22,12 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.poll.api.PollAnswerItem import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_10 import io.element.android.libraries.matrix.test.A_USER_ID_2 @@ -49,14 +51,14 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - not ended, no votes`() = runTest { - Truth.assertThat(factory.create(aPollContent())).isEqualTo(aTimelineItemPollContent()) + Truth.assertThat(factory.create(aPollContent(), eventId = null)).isEqualTo(aTimelineItemPollContent()) } @Test fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes)) + factory.create(aPollContent(votes = votes), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -73,7 +75,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Disclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(endTime = 1UL)) + factory.create(aPollContent(endTime = 1UL), eventId = null) ).isEqualTo( aTimelineItemPollContent().let { it.copy( @@ -88,7 +90,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes, endTime = 1UL)) + factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -107,7 +109,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(votes = votes, endTime = 1UL)) + factory.create(aPollContent(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -125,9 +127,9 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - not ended, no votes`() = runTest { Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy()) + factory.create(aPollContent(PollKind.Undisclosed).copy(), eventId = null) ).isEqualTo( - aTimelineItemPollContent(PollKind.Undisclosed).let { + aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let { it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) } ) @@ -137,7 +139,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -155,7 +157,7 @@ internal class TimelineItemContentPollFactoryTest { @Test fun `Undisclosed poll - ended, no votes, no winner`() = runTest { Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL), eventId = null) ).isEqualTo( aTimelineItemPollContent().let { it.copy( @@ -173,7 +175,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL)) + factory.create(aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -193,7 +195,7 @@ internal class TimelineItemContentPollFactoryTest { fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id } Truth.assertThat( - factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL)) + factory.create(aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), eventId = null) ) .isEqualTo( aTimelineItemPollContent( @@ -209,6 +211,15 @@ internal class TimelineItemContentPollFactoryTest { ) } + @Test + fun `eventId is populated`() = runTest { + Truth.assertThat(factory.create(aPollContent(), eventId = null)) + .isEqualTo(aTimelineItemPollContent(eventId = null)) + + Truth.assertThat(factory.create(aPollContent(), eventId = AN_EVENT_ID)) + .isEqualTo(aTimelineItemPollContent(eventId = AN_EVENT_ID)) + } + private fun aPollContent( pollKind: PollKind = PollKind.Disclosed, votes: Map> = emptyMap(), @@ -223,6 +234,7 @@ internal class TimelineItemContentPollFactoryTest { ) private fun aTimelineItemPollContent( + eventId: EventId? = null, pollKind: PollKind = PollKind.Disclosed, answerItems: List = listOf( aPollAnswerItem(A_POLL_ANSWER_1), @@ -232,6 +244,7 @@ internal class TimelineItemContentPollFactoryTest { ), isEnded: Boolean = false, ) = TimelineItemPollContent( + eventId = eventId, question = A_POLL_QUESTION, answerItems = answerItems, pollKind = pollKind, diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index 419aa21204..438c14456c 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.theme.ElementTheme @@ -43,13 +44,18 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun PollContentView( + eventId: EventId?, question: String, answerItems: ImmutableList, pollKind: PollKind, isPollEnded: Boolean, - onAnswerSelected: (PollAnswer) -> Unit, + onAnswerSelected: (pollStartId: EventId, answerId: String) -> Unit, modifier: Modifier = Modifier, ) { + fun onAnswerSelected(pollAnswer: PollAnswer) { + eventId?.let { onAnswerSelected(it, pollAnswer.id) } + } + Column( modifier = modifier .selectableGroup() @@ -58,7 +64,7 @@ fun PollContentView( ) { PollTitle(title = question) - PollAnswers(answerItems = answerItems, onAnswerSelected = onAnswerSelected) + PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) when { isPollEnded || pollKind == PollKind.Disclosed -> DisclosedPollBottomNotice(answerItems) @@ -134,11 +140,12 @@ fun ColumnScope.UndisclosedPollBottomNotice(modifier: Modifier = Modifier) { @Composable internal fun PollContentUndisclosedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isDisclosed = false), pollKind = PollKind.Undisclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } @@ -146,11 +153,12 @@ internal fun PollContentUndisclosedPreview() = ElementPreview { @Composable internal fun PollContentDisclosedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(), pollKind = PollKind.Disclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } @@ -158,10 +166,11 @@ internal fun PollContentDisclosedPreview() = ElementPreview { @Composable internal fun PollContentEndedPreview() = ElementPreview { PollContentView( + eventId = EventId("\$anEventId"), question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isEnded = true), pollKind = PollKind.Disclosed, isPollEnded = false, - onAnswerSelected = { }, + onAnswerSelected = { _, _ -> }, ) } From 1d3d1fe48072e5a9333198687018f806433bddb2 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Wed, 30 Aug 2023 19:02:37 +0200 Subject: [PATCH 48/75] Fix the orientation of sent images (#1190) * Fix the orientation of sent images --------- Co-authored-by: Benoit Marty --- changelog.d/1135.bugfix | 1 + .../libraries/androidutils/bitmap/Bitmap.kt | 19 +++++++------------ .../mediaupload/AndroidMediaPreProcessor.kt | 7 +++++++ .../libraries/mediaupload/ImageCompressor.kt | 11 +++++++---- 4 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 changelog.d/1135.bugfix diff --git a/changelog.d/1135.bugfix b/changelog.d/1135.bugfix new file mode 100644 index 0000000000..2b963c7732 --- /dev/null +++ b/changelog.d/1135.bugfix @@ -0,0 +1 @@ +Fix the orientation of sent images. diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt index 6f8aa76d03..c3b7e3110e 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/bitmap/Bitmap.kt @@ -22,7 +22,6 @@ import android.graphics.Matrix import androidx.core.graphics.scale import androidx.exifinterface.media.ExifInterface import java.io.File -import java.io.InputStream import kotlin.math.min fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) { @@ -32,13 +31,6 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int } } -/** - * Reads the EXIF metadata from the [inputStream] and rotates the current [Bitmap] to match it. - * @return The resulting [Bitmap] or `null` if no metadata was found. - */ -fun Bitmap.rotateToMetadataOrientation(inputStream: InputStream): Result = - runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) } - /** * Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio. * @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0. @@ -77,8 +69,11 @@ fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight return inSampleSize } -private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInterface): Bitmap { - val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) +/** + * Decodes the [inputStream] into a [Bitmap] and applies the needed rotation based on [orientation]. + * This orientation value must be one of `ExifInterface.ORIENTATION_*` constants. + */ +fun Bitmap.rotateToMetadataOrientation(orientation: Int): Bitmap { val matrix = Matrix() when (orientation) { ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) @@ -94,8 +89,8 @@ private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInter matrix.preRotate(90f) matrix.preScale(-1f, 1f) } - else -> return bitmap + else -> return this } - return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 8ff40fae39..9fc160252b 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -119,10 +119,17 @@ class AndroidMediaPreProcessor @Inject constructor( private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { suspend fun processImageWithCompression(): MediaUploadInfo { + // Read the orientation metadata from its own stream. Trying to reuse this stream for compression will fail. + val orientation = contentResolver.openInputStream(uri).use { input -> + val exifInterface = input?.let { ExifInterface(it) } + exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) + } ?: ExifInterface.ORIENTATION_UNDEFINED + val compressionResult = contentResolver.openInputStream(uri).use { input -> imageCompressor.compressToTmpFile( inputStream = requireNotNull(input), resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), + orientation = orientation, ).getOrThrow() } val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index ab30f67b65..a619a27bd9 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.mediaupload import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import androidx.exifinterface.media.ExifInterface import io.element.android.libraries.androidutils.bitmap.calculateInSampleSize import io.element.android.libraries.androidutils.bitmap.resizeToMax import io.element.android.libraries.androidutils.bitmap.rotateToMetadataOrientation @@ -37,17 +38,18 @@ class ImageCompressor @Inject constructor( /** * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode], then writes it into a - * temporary file using the passed [format] and [desiredQuality]. + * temporary file using the passed [format], [orientation] and [desiredQuality]. * @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata. */ suspend fun compressToTmpFile( inputStream: InputStream, resizeMode: ResizeMode, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + orientation: Int = ExifInterface.ORIENTATION_UNDEFINED, desiredQuality: Int = 80, ): Result = withContext(Dispatchers.IO) { runCatching { - val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow() + val compressedBitmap = compressToBitmap(inputStream, resizeMode, orientation).getOrThrow() // Encode bitmap to the destination temporary file val tmpFile = context.createTmpFile(extension = "jpeg") tmpFile.outputStream().use { @@ -63,19 +65,20 @@ class ImageCompressor @Inject constructor( } /** - * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode]. + * Decodes the [inputStream] into a [Bitmap] and applies the needed transformations (rotation, scale) based on [resizeMode] and [orientation]. * @return a [Result] containing the resulting [Bitmap]. */ fun compressToBitmap( inputStream: InputStream, resizeMode: ResizeMode, + orientation: Int, ): Result = runCatching { BufferedInputStream(inputStream).use { input -> val options = BitmapFactory.Options() calculateDecodingScale(input, resizeMode, options) val decodedBitmap = BitmapFactory.decodeStream(input, null, options) ?: error("Decoding Bitmap from InputStream failed") - val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(input).getOrThrow() + val rotatedBitmap = decodedBitmap.rotateToMetadataOrientation(orientation) if (resizeMode is ResizeMode.Strict) { rotatedBitmap.resizeToMax(resizeMode.maxWidth, resizeMode.maxHeight) } else { From 50869d7b28e2bdc238f677e5aa088380486bd321 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 31 Aug 2023 12:08:21 +0200 Subject: [PATCH 49/75] Focus on question field when opening screen. (#1194) --- .../poll/impl/create/CreatePollView.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index 3e3ec4eb16..ceb9513545 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -25,12 +25,19 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.poll.impl.R @@ -68,6 +75,10 @@ fun CreatePollView( onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } ) + val questionFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + questionFocusRequester.requestFocus() + } Scaffold( modifier = modifier, topBar = { @@ -113,10 +124,13 @@ fun CreatePollView( onValueChange = { state.eventSink(CreatePollEvents.SetQuestion(it)) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .focusRequester(questionFocusRequester) + .fillMaxWidth(), placeholder = { Text(text = stringResource(id = R.string.screen_create_poll_question_hint)) }, + keyboardOptions = keyboardOptions, ) } ) @@ -133,6 +147,7 @@ fun CreatePollView( placeholder = { Text(text = stringResource(id = R.string.screen_create_poll_answer_hint, index + 1)) }, + keyboardOptions = keyboardOptions, ) }, trailingContent = ListItemContent.Custom { @@ -185,3 +200,8 @@ internal fun CreatePollViewPreview( state = state, ) } + +private val keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + imeAction = ImeAction.Next, +) From c0a4ca16a25f6b99391bf2c2bca32ae622dfbc6a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:35:31 +0200 Subject: [PATCH 50/75] Update dagger to v2.48 (#1193) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0d52d1e0df..863d4812b8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ sqldelight = "1.5.5" telephoto = "0.6.0-SNAPSHOT" # DI -dagger = "2.47" +dagger = "2.48" anvil = "2.4.7-1-8" # Auto service From ecf2d0692863e6d08d2aaa27d296d3e98dd281aa Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 31 Aug 2023 11:37:08 +0100 Subject: [PATCH 51/75] Fix colon aligment and load emojis lazily. - Fix colon aligment - Load emojis lazily. --- .../components/customreaction/CustomReactionPresenter.kt | 2 +- .../components/customreaction/DefaultEmojibaseProvider.kt | 8 +++++--- .../components/customreaction/EmojibaseProvider.kt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) 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 6831ca7dfa..b048383b1f 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 @@ -43,7 +43,7 @@ class CustomReactionPresenter @Inject constructor( localCoroutineScope.launch { target.value = CustomReactionState.Target.Success( event = event, - emojibaseStore = emojibaseProvider.loadEmojibase() + emojibaseStore = emojibaseProvider.emojibaseStore ) } } 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 index 2e66ea381e..a68d6f0d4e 100644 --- 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 @@ -20,8 +20,10 @@ 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) +class DefaultEmojibaseProvider(val context: Context): EmojibaseProvider { + + override val emojibaseStore: EmojibaseStore by lazy { + EmojibaseDatasource().load(context) } + } 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 index 39739538d2..6a4f48a806 100644 --- 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 @@ -19,5 +19,5 @@ package io.element.android.features.messages.impl.timeline.components.customreac import io.element.android.emojibasebindings.EmojibaseStore interface EmojibaseProvider { - fun loadEmojibase(): EmojibaseStore + val emojibaseStore: EmojibaseStore } From b16faeceba66b721fa0c7b15a2955138066cda52 Mon Sep 17 00:00:00 2001 From: David Langley Date: Thu, 31 Aug 2023 11:41:06 +0100 Subject: [PATCH 52/75] Fix FakeEmojibaseProvider --- .../components/customreaction/FakeEmojibaseProvider.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 434212030a..7bf993e2a4 100644 --- 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 @@ -20,7 +20,6 @@ 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()) - } + override val emojibaseStore: EmojibaseStore + get() = EmojibaseStore(mapOf()) } From d4ae0ec226faf0c96f3bd69f49f19953781b7e87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 10:54:01 +0000 Subject: [PATCH 53/75] Update mobile-dev-inc/action-maestro-cloud action to v1.5.0 --- .github/workflows/maestro.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maestro.yml b/.github/workflows/maestro.yml index 74fb1cfc83..46ee84896f 100644 --- a/.github/workflows/maestro.yml +++ b/.github/workflows/maestro.yml @@ -40,7 +40,7 @@ jobs: ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} - - uses: mobile-dev-inc/action-maestro-cloud@v1.4.1 + - uses: mobile-dev-inc/action-maestro-cloud@v1.5.0 with: api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} # Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android): From 47a0ecb3b8196323c1b8e187acbe88a986b0d2ee Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 31 Aug 2023 12:58:37 +0200 Subject: [PATCH 54/75] Protect `TimelineItemAspectRatioBox` against `Float.NaN` (#1201) * Protect `TimelineItemAspectRatioBox` against infinite values. --- changelog.d/1995.bugfix | 1 + .../factories/event/TimelineItemContentMessageFactory.kt | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1995.bugfix diff --git a/changelog.d/1995.bugfix b/changelog.d/1995.bugfix new file mode 100644 index 0000000000..acec25add0 --- /dev/null +++ b/changelog.d/1995.bugfix @@ -0,0 +1 @@ +Crash with `aspectRatio` modifier when `Float.NaN` was used as input. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 9da31ee6a7..040969e092 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -132,10 +132,12 @@ class TimelineItemContentMessageFactory @Inject constructor( } private fun aspectRatioOf(width: Long?, height: Long?): Float? { - return if (height != null && width != null) { + val result = if (height != null && width != null) { width.toFloat() / height.toFloat() } else { null } + + return result?.takeIf { it.isFinite() } } } From 199f578e4a795687f3dbb8e13f8f37daed32baef Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 31 Aug 2023 13:37:20 +0200 Subject: [PATCH 55/75] Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications (#1199) * Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications. * Add feature flag --- changelog.d/1198.bugfix | 1 + .../impl/developer/DeveloperSettingsPresenter.kt | 10 ++++++++-- .../android/libraries/featureflag/api/FeatureFlags.kt | 6 ++++++ .../featureflag/impl/BuildtimeFeatureFlagProvider.kt | 1 + libraries/matrix/impl/build.gradle.kts | 1 + .../libraries/matrix/impl/RustMatrixClientFactory.kt | 10 +++++++++- samples/minimal/build.gradle.kts | 1 + .../io/element/android/samples/minimal/MainActivity.kt | 4 +++- 8 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 changelog.d/1198.bugfix diff --git a/changelog.d/1198.bugfix b/changelog.d/1198.bugfix new file mode 100644 index 0000000000..6ef69c4eff --- /dev/null +++ b/changelog.d/1198.bugfix @@ -0,0 +1 @@ +Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications. diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 010e17bd35..5f50fde309 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -83,7 +83,8 @@ class DeveloperSettingsPresenter @Inject constructor( features, enabledFeatures, event.feature, - event.isEnabled + event.isEnabled, + triggerClearCache = { handleEvents(DeveloperSettingsEvents.ClearCache) } ) DeveloperSettingsEvents.ClearCache -> coroutineScope.clearCache(clearCacheAction) } @@ -122,12 +123,17 @@ class DeveloperSettingsPresenter @Inject constructor( features: SnapshotStateMap, enabledFeatures: SnapshotStateMap, featureUiModel: FeatureUiModel, - enabled: Boolean + enabled: Boolean, + triggerClearCache: () -> Unit, ) = launch { val feature = features[featureUiModel.key] ?: return@launch if (featureFlagService.setFeatureEnabled(feature, enabled)) { enabledFeatures[featureUiModel.key] = enabled } + + if (featureUiModel.key == FeatureFlags.UseEncryptionSync.key) { + triggerClearCache() + } } private fun CoroutineScope.computeCacheSize(cacheSize: MutableState>) = launch { diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index e1f4b740b0..08ac0ae039 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -31,5 +31,11 @@ enum class FeatureFlags( title = "Polls", description = "Create poll and render poll events in the timeline", defaultValue = false, + ), + UseEncryptionSync( + key = "feature.useencryptionsync", + title = "Use encryption sync", + description = "Use the encryption sync API for decrypting notifications.", + defaultValue = true, ) } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt index 83913cbac5..5640d108a6 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/BuildtimeFeatureFlagProvider.kt @@ -31,6 +31,7 @@ class BuildtimeFeatureFlagProvider @Inject constructor() : when (feature) { FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> false + FeatureFlags.UseEncryptionSync -> true } } else { false diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index b2a21f26df..a2b616f989 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.services.toolbox.api) + implementation(projects.libraries.featureflag.api) api(projects.libraries.matrix.api) implementation(libs.dagger) implementation(projects.libraries.core) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index bb80032230..3615115bb4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.impl import android.content.Context import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -39,6 +41,7 @@ class RustMatrixClientFactory @Inject constructor( private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, private val clock: SystemClock, + private val featureFlagsService: FeatureFlagService, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { @@ -53,7 +56,12 @@ class RustMatrixClientFactory @Inject constructor( client.restoreSession(sessionData.toSession()) - val syncService = client.syncService().finish() + val syncService = client.syncService().apply { + if (featureFlagsService.isFeatureEnabled(FeatureFlags.UseEncryptionSync)) { + withEncryptionSync(withCrossProcessLock = false, appIdentifier = null) + } + } + .finish() RustMatrixClient( client = client, diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 8063ac9b0a..1473bd7f93 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.features.login.impl) implementation(projects.features.networkmonitor.impl) implementation(projects.services.toolbox.impl) + implementation(projects.libraries.featureflag.impl) implementation(libs.coroutines.core) coreLibraryDesugaring(libs.android.desugar) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index a915e70046..94cd28f021 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowCompat +import io.element.android.libraries.featureflag.impl.DefaultFeatureFlagService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.impl.RustMatrixClientFactory import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService @@ -54,7 +55,8 @@ class MainActivity : ComponentActivity() { coroutineDispatchers = Singleton.coroutineDispatchers, sessionStore = sessionStore, userAgentProvider = userAgentProvider, - clock = DefaultSystemClock() + clock = DefaultSystemClock(), + featureFlagsService = DefaultFeatureFlagService(emptySet()) ) ) } From 3a920f1a9dab02fbfdff9220377a404c4ba1307b Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 31 Aug 2023 14:39:11 +0200 Subject: [PATCH 56/75] Poll end (#1182) - Adds an "End Poll" item in the action list long press menu. - Shows only on remote polls that have not ended yet and only if the user is the creator or has redact powers. Closes https://github.com/vector-im/element-meta/issues/2026 --- .../messages/impl/MessagesPresenter.kt | 6 ++ .../impl/actionlist/ActionListPresenter.kt | 12 +++- .../actionlist/ActionListStateProvider.kt | 18 ++++++ .../actionlist/model/TimelineItemAction.kt | 1 + .../TimelineItemLocationContentProvider.kt | 33 +++++++++++ .../messages/MessagesPresenterTest.kt | 19 +++++++ .../actionlist/ActionListPresenterTest.kt | 56 +++++++++---------- .../poll/impl/create/CreatePollView.kt | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../libraries/designsystem/VectorIcons.kt | 1 + .../src/main/res/drawable/ic_done_24.xml | 10 ++++ .../src/main/res/values/localazy.xml | 1 + ...etContent-D-0_1_null_8,NEXUS_5,1.0,en].png | 3 + ...etContent-N-0_2_null_8,NEXUS_5,1.0,en].png | 3 + 14 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 libraries/designsystem/src/main/res/drawable/ic_done_24.xml create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 8a374471e3..5f67fd1f7c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -202,6 +202,7 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> handleReportAction(targetEvent) + TimelineItemAction.EndPoll -> handleEndPollAction(targetEvent) } } @@ -310,6 +311,11 @@ class MessagesPresenter @AssistedInject constructor( navigator.onReportContentClicked(event.eventId, event.senderId) } + private suspend fun handleEndPollAction(event: TimelineItem.Event) { + event.eventId?.let { room.endPoll(it, "The poll with event id: $it has ended.") } + // TODO Polls: Send poll end analytic + } + private suspend fun handleCopyContents(event: TimelineItem.Event) { val content = when (event.content) { is TimelineItemTextBasedContent -> event.content.body diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index e654365bcd..f5e28818c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -99,6 +99,16 @@ class ActionListPresenter @Inject constructor( } is TimelineItemPollContent -> { buildList { + val isMineOrCanRedact = timelineItem.isMine || userCanRedact + + // TODO Poll: Reply to poll + // if (timelineItem.isRemote) { + // // Can only reply or forward messages already uploaded to the server + // add(TimelineItemAction.Reply) + // } + if (!timelineItem.content.isEnded && timelineItem.isRemote && isMineOrCanRedact) { + add(TimelineItemAction.EndPoll) + } if (timelineItem.content.canBeCopied()) { add(TimelineItemAction.Copy) } @@ -108,7 +118,7 @@ class ActionListPresenter @Inject constructor( if (!timelineItem.isMine) { add(TimelineItemAction.ReportContent) } - if (timelineItem.isMine || userCanRedact) { + if (isMineOrCanRedact) { add(TimelineItemAction.Redact) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 09213d64b3..755d4c36e1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemLocationContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVideoContent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -83,6 +84,15 @@ open class ActionListStateProvider : PreviewParameterProvider { ), displayEmojiReactions = false, ), + anActionListState().copy( + target = ActionListState.Target.Success( + event = aTimelineItemEvent(content = aTimelineItemPollContent()).copy( + reactionsState = reactionsState + ), + actions = aTimelineItemPollActionList(), + ), + displayEmojiReactions = false, + ), ) } } @@ -104,3 +114,11 @@ fun aTimelineItemActionList(): ImmutableList { TimelineItemAction.Developer, ) } +fun aTimelineItemPollActionList(): ImmutableList { + return persistentListOf( + TimelineItemAction.Reply, + TimelineItemAction.EndPoll, + TimelineItemAction.Developer, + TimelineItemAction.Redact, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index b6141218eb..8dc48f892e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -35,4 +35,5 @@ sealed class TimelineItemAction( data object Edit : TimelineItemAction(CommonStrings.action_edit, VectorIcons.Edit) data object Developer : TimelineItemAction(CommonStrings.action_view_source, VectorIcons.DeveloperMode) data object ReportContent : TimelineItemAction(CommonStrings.action_report_content, VectorIcons.ReportContent, destructive = true) + data object EndPoll : TimelineItemAction(CommonStrings.action_end_poll, VectorIcons.EndPoll) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt index a21f262071..7ad79f19e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemLocationContentProvider.kt @@ -18,6 +18,10 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.location.api.Location +import io.element.android.features.poll.api.PollAnswerItem +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind open class TimelineItemLocationContentProvider : PreviewParameterProvider { override val values: Sequence @@ -36,3 +40,32 @@ fun aTimelineItemLocationContent(description: String? = null) = TimelineItemLoca ), description = description, ) + +fun aTimelineItemPollContent( + isEnded: Boolean = false, +) = TimelineItemPollContent( + eventId = EventId("\$anEventId"), + question = "Some question?", + answerItems = listOf( + PollAnswerItem( + answer = PollAnswer("id_1", "Answer1"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + PollAnswerItem( + answer = PollAnswer("id_2", "Answer2"), + isSelected = false, + isEnabled = false, + isWinner = false, + isDisclosed = false, + votesCount = 0, + percentage = 0.0f, + ), + ), + pollKind = PollKind.Disclosed, + isEnded = isEnded, +) 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..f5d0185789 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 @@ -72,6 +72,7 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers +import io.element.android.tests.testutils.waitForPredicate import io.mockk.mockk import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -559,6 +560,24 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle poll end`() = runTest { + val room = FakeMatrixRoom() + val presenter = createMessagePresenter(matrixRoom = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(MessagesEvents.HandleAction(TimelineItemAction.EndPoll, aMessageEvent())) + waitForPredicate { room.endPollInvocations.size == 1 } + cancelAndIgnoreRemainingEvents() + assertThat(room.endPollInvocations.size).isEqualTo(1) + assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) + assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.") + // TODO Polls: Test poll end analytic + } + } + private fun TestScope.createMessagePresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt index 111b3a370d..a4ff484ff3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/actionlist/ActionListPresenterTest.kt @@ -26,15 +26,11 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter 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.aTimelineItemEvent -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemImageContent +import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent -import io.element.android.features.poll.api.PollAnswerItem -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.poll.PollAnswer -import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.core.aBuildMeta import kotlinx.collections.immutable.persistentListOf @@ -384,34 +380,34 @@ class ActionListPresenterTest { val initialState = awaitItem() val messageEvent = aMessageEvent( isMine = true, - content = TimelineItemPollContent( - eventId = EventId("\$anEventId"), - question = "Some question?", - answerItems = listOf( - PollAnswerItem( - answer = PollAnswer("id_1", "Answer1"), - isSelected = false, - isEnabled = false, - isWinner = false, - isDisclosed = false, - votesCount = 0, - percentage = 0.0f, - ), - PollAnswerItem( - answer = PollAnswer("id_2", "Answer2"), - isSelected = false, - isEnabled = false, - isWinner = false, - isDisclosed = false, - votesCount = 0, - percentage = 0.0f, - ), - ), - pollKind = PollKind.Disclosed, - isEnded = false, + content = aTimelineItemPollContent(), + ) + initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) + val successState = awaitItem() + assertThat(successState.target).isEqualTo( + ActionListState.Target.Success( + messageEvent, + persistentListOf( + TimelineItemAction.EndPoll, + TimelineItemAction.Redact, + ) ) ) + assertThat(successState.displayEmojiReactions).isTrue() + } + } + @Test + fun `present - compute for ended poll message`() = runTest { + val presenter = anActionListPresenter(isBuildDebuggable = false) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val messageEvent = aMessageEvent( + isMine = true, + content = aTimelineItemPollContent(isEnded = true), + ) initialState.eventSink.invoke(ActionListEvents.ComputeForMessage(messageEvent, false)) val successState = awaitItem() assertThat(successState.target).isEqualTo( diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt index ceb9513545..8e3de07574 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/create/CreatePollView.kt @@ -71,7 +71,7 @@ fun CreatePollView( val navBack = { state.eventSink(CreatePollEvents.ConfirmNavBack) } BackHandler(onBack = navBack) if (state.showConfirmation) ConfirmationDialog( - content = stringResource(id = R.string.screen_create_poll_confirmation), + content = stringResource(id = R.string.screen_create_poll_discard_confirmation), onSubmitClicked = { state.eventSink(CreatePollEvents.NavBack) }, onDismiss = { state.eventSink(CreatePollEvents.HideConfirmation) } ) diff --git a/features/poll/impl/src/main/res/values/localazy.xml b/features/poll/impl/src/main/res/values/localazy.xml index 846b247108..876dd0ee44 100644 --- a/features/poll/impl/src/main/res/values/localazy.xml +++ b/features/poll/impl/src/main/res/values/localazy.xml @@ -4,7 +4,7 @@ "Show results only after poll ends" "Anonymous Poll" "Option %1$d" - "Are you sure you would like to go back?" + "Are you sure you would like to go back?" "Question or topic" "What is the poll about?" "Create Poll" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 47b951baa2..51755ef966 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -27,4 +27,5 @@ object VectorIcons { val ReportContent = R.drawable.ic_report_content val Groups = R.drawable.ic_groups val Share = R.drawable.ic_share + val EndPoll = R.drawable.ic_done_24 } diff --git a/libraries/designsystem/src/main/res/drawable/ic_done_24.xml b/libraries/designsystem/src/main/res/drawable/ic_done_24.xml new file mode 100644 index 0000000000..280f0bd804 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_done_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 4f25e8f07b..95ae43dd21 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -61,6 +61,7 @@ "Take photo" "View Source" "Yes" + "End poll" "About" "Acceptable use policy" "Analytics" diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6ac8e597c5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b0cc63408163b06093d0c8d238eb9451bf526af518c8f09c53a96b33d72c10f +size 23135 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..11192bb213 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7026daf71085ac6c62b66f93c1002afd1f5bb28446aa896fac8b11f2750ac80c +size 22317 From 582705139edbe7127d5ddb1aadee2fa6e6072e40 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 29 Aug 2023 16:45:14 +0200 Subject: [PATCH 57/75] Poll: Maestro tests --- .maestro/tests/roomList/timeline/messages/poll.yaml | 13 +++++++++++++ .maestro/tests/roomList/timeline/timeline.yaml | 1 + 2 files changed, 14 insertions(+) create mode 100644 .maestro/tests/roomList/timeline/messages/poll.yaml diff --git a/.maestro/tests/roomList/timeline/messages/poll.yaml b/.maestro/tests/roomList/timeline/messages/poll.yaml new file mode 100644 index 0000000000..65495dda60 --- /dev/null +++ b/.maestro/tests/roomList/timeline/messages/poll.yaml @@ -0,0 +1,13 @@ +appId: ${APP_ID} +--- +- takeScreenshot: build/maestro/530-Timeline +- tapOn: "Add attachment" +- tapOn: "Poll" +- tapOn: "What is the poll about?" +- inputText: "I am a poll" +- tapOn: "Option 1" +- inputText: "Answer 1" +- tapOn: "Option 2" +- inputText: "Answer 2" +- tapOn: "Create" +- takeScreenshot: build/maestro/531-Timeline diff --git a/.maestro/tests/roomList/timeline/timeline.yaml b/.maestro/tests/roomList/timeline/timeline.yaml index bec566985d..1acb10a9aa 100644 --- a/.maestro/tests/roomList/timeline/timeline.yaml +++ b/.maestro/tests/roomList/timeline/timeline.yaml @@ -5,5 +5,6 @@ appId: ${APP_ID} - takeScreenshot: build/maestro/500-Timeline - runFlow: messages/text.yaml - runFlow: messages/location.yaml +- runFlow: messages/poll.yaml - back - runFlow: ../../assertions/assertHomeDisplayed.yaml From 33683abf5914df026b37993f12b03fc4d0486f23 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 31 Aug 2023 15:17:11 +0200 Subject: [PATCH 58/75] Enable polls (#1196) Enable the Polls feature. Allows to create, view, vote and end polls. --- changelog.d/1196.feature | 1 + .../io/element/android/libraries/featureflag/api/FeatureFlags.kt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/1196.feature diff --git a/changelog.d/1196.feature b/changelog.d/1196.feature new file mode 100644 index 0000000000..fe1eb354f9 --- /dev/null +++ b/changelog.d/1196.feature @@ -0,0 +1 @@ +Enable the Polls feature. Allows to create, view, vote and end polls. diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index 08ac0ae039..312745a4df 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -30,7 +30,6 @@ enum class FeatureFlags( key = "feature.polls", title = "Polls", description = "Create poll and render poll events in the timeline", - defaultValue = false, ), UseEncryptionSync( key = "feature.useencryptionsync", From 09871c712d3b01f803095e96c8bc8b191d150455 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 31 Aug 2023 14:17:14 +0100 Subject: [PATCH 59/75] Update Localazy readme. --- tools/localazy/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/localazy/README.md b/tools/localazy/README.md index b0b5c7e980..2da7afd8bb 100644 --- a/tools/localazy/README.md +++ b/tools/localazy/README.md @@ -26,13 +26,15 @@ Never edit manually the files `localazy.xml` or `translations.xml`!. For code clarity and in order to download strings to the correct module, here are some naming rules to follow as much as possible: -- Keys for common strings, i.e. strings that can be used at multiple places must start by `action_` if this is a verb, or `common_` if not; -- Keys for common accessibility strings must start by `a11y_`. Example: `a11y_hide_password`; +- Keys for common strings, i.e. strings that can be used at multiple places must start by `action.` if this is a verb, or `common.` if not; +- Keys for common accessibility strings must start by `a11y.`. Example: `a11y.hide_password`; +- Keys for common strings should be named to match the string. Example: `action.copy_link` for the string `Copy link`; +- When creating common strings, make sure to enable "Use dot (.) to create nested keys"; - Keys for strings used in a single screen must start with `screen_` followed by the screen name, followed by a free name. Example: `screen_onboarding_welcome_title`; - Keys can have `_title` or `_subtitle` suffixes. Example: `screen_onboarding_welcome_title`, `screen_change_server_subtitle`; - For dialogs, keys can have `_dialog_title`, `_dialog_content`, and `_dialog_submit` suffixes. Example: `screen_signout_confirmation_dialog_title`, `screen_signout_confirmation_dialog_content`, `screen_signout_confirmation_dialog_submit`; -- `a11y_` pattern can be used for strings that are only used for accessibility. Example: `a11y_hide_password`, `screen_roomlist_a11y_create_message`; -- Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`. +- `a11y.` pattern can be used for strings that are only used for accessibility. Example: `a11y.hide_password`, `screen_roomlist_a11y_create_message`; +- Strings for error message can start by `error_`, or contain `_error_` if used in a specific screen only. Example: `error_some_messages_have_not_been_sent`, `screen_change_server_error_invalid_homeserver`; *Note*: those rules applies for `strings` and for `plurals`. From 00828b671442769ff781b4f8f45149adc52024b3 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 31 Aug 2023 15:40:51 +0200 Subject: [PATCH 60/75] New icon for "poll end" in both action menu and timeline item. (#1203) - Also fixes preview of action list (reply is included as it will soon be added). --- .../actionlist/ActionListStateProvider.kt | 4 +++- .../features/poll/api/PollContentView.kt | 24 +++++++++++++------ .../libraries/designsystem/VectorIcons.kt | 2 +- .../src/main/res/drawable/ic_done_24.xml | 10 -------- .../src/main/res/drawable/ic_poll_end.xml | 14 +++++++++++ ...etContent-D-0_1_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...etContent-N-0_2_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...ontentEnded-D-2_2_null,NEXUS_5,1.0,en].png | 4 ++-- ...ontentEnded-N-2_3_null,NEXUS_5,1.0,en].png | 4 ++-- 9 files changed, 43 insertions(+), 27 deletions(-) delete mode 100644 libraries/designsystem/src/main/res/drawable/ic_done_24.xml create mode 100644 libraries/designsystem/src/main/res/drawable/ic_poll_end.xml diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index 755d4c36e1..bb9c851288 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -116,9 +116,11 @@ fun aTimelineItemActionList(): ImmutableList { } fun aTimelineItemPollActionList(): ImmutableList { return persistentListOf( - TimelineItemAction.Reply, TimelineItemAction.EndPoll, + TimelineItemAction.Reply, + TimelineItemAction.Copy, TimelineItemAction.Developer, + TimelineItemAction.ReportContent, TimelineItemAction.Redact, ) } diff --git a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt index 438c14456c..9301630b73 100644 --- a/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt +++ b/features/poll/api/src/main/kotlin/io/element/android/features/poll/api/PollContentView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.VectorIcons import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Icon @@ -62,7 +63,7 @@ fun PollContentView( .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - PollTitle(title = question) + PollTitle(title = question, isPollEnded = isPollEnded) PollAnswers(answerItems = answerItems, onAnswerSelected = ::onAnswerSelected) @@ -76,17 +77,26 @@ fun PollContentView( @Composable internal fun PollTitle( title: String, + isPollEnded: Boolean, modifier: Modifier = Modifier ) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Icon( - modifier = Modifier.size(22.dp), - imageVector = Icons.Outlined.Poll, - contentDescription = null - ) + if (isPollEnded) { + Icon( + resourceId = VectorIcons.EndPoll, + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + } else { + Icon( + imageVector = Icons.Outlined.Poll, + contentDescription = null, + modifier = Modifier.size(22.dp) + ) + } Text( text = title, style = ElementTheme.typography.fontBodyLgMedium @@ -170,7 +180,7 @@ internal fun PollContentEndedPreview() = ElementPreview { question = "What type of food should we have at the party?", answerItems = aPollAnswerItemList(isEnded = true), pollKind = PollKind.Disclosed, - isPollEnded = false, + isPollEnded = true, onAnswerSelected = { _, _ -> }, ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 51755ef966..4f06b3ebcb 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -27,5 +27,5 @@ object VectorIcons { val ReportContent = R.drawable.ic_report_content val Groups = R.drawable.ic_groups val Share = R.drawable.ic_share - val EndPoll = R.drawable.ic_done_24 + val EndPoll = R.drawable.ic_poll_end } diff --git a/libraries/designsystem/src/main/res/drawable/ic_done_24.xml b/libraries/designsystem/src/main/res/drawable/ic_done_24.xml deleted file mode 100644 index 280f0bd804..0000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_done_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml b/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml new file mode 100644 index 0000000000..21d895e613 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_poll_end.xml @@ -0,0 +1,14 @@ + + + + diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png index 6ac8e597c5..cb2e25b07f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-D-0_1_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b0cc63408163b06093d0c8d238eb9451bf526af518c8f09c53a96b33d72c10f -size 23135 +oid sha256:3598515dc84276014c2f3827f533c808a2683191a67e7a68191501bb848b8293 +size 28326 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png index 11192bb213..0311150ab0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.actionlist_null_SheetContent-N-0_2_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7026daf71085ac6c62b66f93c1002afd1f5bb28446aa896fac8b11f2750ac80c -size 22317 +oid sha256:d00152b1ff92d93564af11537dcb8d77116e943cfa2b45dee2c66260284b8807 +size 26875 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png index f148a727ae..140c07a3ca 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-D-2_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a9ccbbc0a208ac398f07f0070a43d0ca5ab67ef322b274b641270a9c21a4a60 -size 48972 +oid sha256:434c4f6dc4ee4c66291eca2ff1a8ab2471aaf521dbbf0d32f53f277ea6519c07 +size 49107 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png index df2a5c30b9..0b32e49a00 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.poll.api_null_PollContentEnded-N-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5736eb1d232b841d62f9d96070fc9b2993453652f5d7c448b6a819db9543615e -size 45756 +oid sha256:63685437c43543115e8f509919ef179d70a620c012c8a46ebf6682dcfe9820c6 +size 45890 From eef5aa19490a786f90476a958543e208856de6b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 31 Aug 2023 15:41:48 +0200 Subject: [PATCH 61/75] fix typo in git url. Co-authored-by: Florian Renaud --- docs/install_from_github_release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install_from_github_release.md b/docs/install_from_github_release.md index 57363a617c..de395f737a 100644 --- a/docs/install_from_github_release.md +++ b/docs/install_from_github_release.md @@ -22,7 +22,7 @@ git clone git@github.com:vector-im/element-x-android.git ``` or ```bash -git clone https://github.com/vector-im/element-x-android.gi +git clone https://github.com/vector-im/element-x-android.git ``` You will also need to install [bundletool](https://developer.android.com/studio/command-line/bundletool). On MacOS, you can run the following command: From d7760003cf8d9beec0c64fcc241b230d50cd84ad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 31 Aug 2023 17:41:00 +0200 Subject: [PATCH 62/75] Remove the log, was causing a crash. kotlin.reflect.jvm.internal.KotlinReflectionInternalError: Function 'handleEvents' (JVM signature: present$handleEvents(Landroidx/compose/runtime/MutableState;Lkotlin/jvm/internal/Ref$ObjectRef;Lio/element/android/libraries/permissions/api/PermissionsEvents;)V) not resolved in class kotlin.jvm.internal.Intrinsics$Kotlin: no members found at kotlin.reflect.jvm.internal.KDeclarationContainerImpl.findFunctionDescriptor(KDeclarationContainerImpl.kt:131) at kotlin.reflect.jvm.internal.KFunctionImpl$descriptor$2.invoke(KFunctionImpl.kt:56) at kotlin.reflect.jvm.internal.KFunctionImpl$descriptor$2.invoke(KFunctionImpl.kt:55) at kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal.invoke(ReflectProperties.java:93) at kotlin.reflect.jvm.internal.ReflectProperties$Val.getValue(ReflectProperties.java:32) at kotlin.reflect.jvm.internal.KFunctionImpl.getDescriptor(KFunctionImpl.kt:55) at kotlin.reflect.jvm.internal.KFunctionImpl.toString(KFunctionImpl.kt:185) at kotlin.jvm.internal.FunctionReference.toString(FunctionReference.java:130) at java.lang.String.valueOf(String.java:4092) at java.lang.StringBuilder.append(StringBuilder.java:179) at io.element.android.libraries.permissions.api.PermissionsState.toString at java.lang.String.valueOf(String.java:4092) at java.lang.StringBuilder.append(StringBuilder.java:179) at io.element.android.libraries.permissions.impl.DefaultPermissionsPresenter.present(DefaultPermissionsPresenter.kt:128) --- .../libraries/permissions/impl/DefaultPermissionsPresenter.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 50012f01b7..f98ab1fb9d 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -124,9 +124,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( permissionAlreadyAsked = isAlreadyAsked, permissionAlreadyDenied = isAlreadyDenied, eventSink = ::handleEvents - ).also { - Timber.tag(loggerTag.value).d("New state: $it") - } + ) } /* From 9b1a30ac8ba84d1410c7b7870610b2207892641a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 07:00:16 +0000 Subject: [PATCH 63/75] Update dependency org.jetbrains.kotlinx:kotlinx-datetime to v0.4.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 549ee745ea..a890f4f46c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ test_core = "1.5.0" #other coil = "2.4.0" -datetime = "0.4.0" +datetime = "0.4.1" serialization_json = "1.6.0" showkase = "1.0.0-beta18" jsoup = "1.16.1" From 3b5fbc659d03f1de7d9f04ad8149b6f3636bc601 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 1 Sep 2023 10:14:35 +0200 Subject: [PATCH 64/75] Fix nightly builds after emojibase integration (#1208) --- app/proguard-rules.pro | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 949ccfce40..e75bf28da3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,3 +32,6 @@ -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** + +# Moshi from EmojiBase +-dontwarn com.google.devtools.ksp.processing.SymbolProcessorProvider From a32e5df1ab7497c2aef4218a32c9d9b9b84834bb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 1 Sep 2023 16:26:39 +0200 Subject: [PATCH 65/75] Developer setting: add entry point to configure the Tracing. Developer setting: add screen to configure log level. Give the custom trace filter to the SDK. --- .../x/initializer/TracingInitializer.kt | 8 +- .../preferences/impl/PreferencesFlowNode.kt | 14 +- .../impl/developer/DeveloperSettingsNode.kt | 10 + .../impl/developer/DeveloperSettingsView.kt | 8 + .../tracing/ConfigureTracingEvents.kt | 25 ++ .../developer/tracing/ConfigureTracingNode.kt | 45 ++++ .../tracing/ConfigureTracingPresenter.kt | 53 ++++ .../tracing/ConfigureTracingState.kt | 25 ++ .../tracing/ConfigureTracingStateProvider.kt | 37 +++ .../developer/tracing/ConfigureTracingView.kt | 247 ++++++++++++++++++ .../tracing/TargetLogLevelMapBuilder.kt | 43 +++ .../tracing/TracingConfigurationStore.kt | 60 +++++ .../tracing/ConfigureTracingPresenterTest.kt | 104 ++++++++ .../InMemoryTracingConfigurationStore.kt | 44 ++++ .../api/tracing/TracingFilterConfiguration.kt | 45 ++-- 15 files changed, 746 insertions(+), 22 deletions(-) create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingEvents.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingNode.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt create mode 100644 features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TracingConfigurationStore.kt create mode 100644 features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt create mode 100644 features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/InMemoryTracingConfigurationStore.kt diff --git a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt index 068d439994..7b93812d35 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/TracingInitializer.kt @@ -17,7 +17,10 @@ package io.element.android.x.initializer import android.content.Context +import androidx.preference.PreferenceManager import androidx.startup.Initializer +import io.element.android.features.preferences.impl.developer.tracing.SharedPrefTracingConfigurationStore +import io.element.android.features.preferences.impl.developer.tracing.TargetLogLevelMapBuilder import io.element.android.libraries.architecture.bindings import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations @@ -34,8 +37,11 @@ class TracingInitializer : Initializer { val bugReporter = appBindings.bugReporter() Timber.plant(tracingService.createTimberTree()) val tracingConfiguration = if (BuildConfig.DEBUG) { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + val store = SharedPrefTracingConfigurationStore(prefs) + val builder = TargetLogLevelMapBuilder(store) TracingConfiguration( - filterConfiguration = TracingFilterConfigurations.debug, + filterConfiguration = TracingFilterConfigurations.custom(builder.getCurrentMap()), writesToLogcat = true, writesToFilesConfiguration = WriteToFilesConfiguration.Disabled ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt index 7e95e0035c..872520105f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/PreferencesFlowNode.kt @@ -33,6 +33,7 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.preferences.impl.about.AboutNode import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode +import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode import io.element.android.features.preferences.impl.root.PreferencesRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -60,6 +61,9 @@ class PreferencesFlowNode @AssistedInject constructor( @Parcelize data object DeveloperSettings : NavTarget + @Parcelize + data object ConfigureTracing : NavTarget + @Parcelize data object AnalyticsSettings : NavTarget @@ -94,7 +98,15 @@ class PreferencesFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } NavTarget.DeveloperSettings -> { - createNode(buildContext) + val callback = object : DeveloperSettingsNode.Callback { + override fun openConfigureTracing() { + backstack.push(NavTarget.ConfigureTracing) + } + } + createNode(buildContext, listOf(callback)) + } + NavTarget.ConfigureTracing -> { + createNode(buildContext) } NavTarget.About -> { createNode(buildContext) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt index d5af758dec..96339e7bb4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsNode.kt @@ -24,6 +24,7 @@ import com.airbnb.android.showkase.models.Showkase import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -37,6 +38,14 @@ class DeveloperSettingsNode @AssistedInject constructor( private val presenter: DeveloperSettingsPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openConfigureTracing() + } + + private fun onOpenConfigureTracing() { + plugins().forEach { it.openConfigureTracing() } + } + @Composable override fun View(modifier: Modifier) { val activity = LocalContext.current as Activity @@ -50,6 +59,7 @@ class DeveloperSettingsNode @AssistedInject constructor( state = state, modifier = modifier, onOpenShowkase = ::openShowkase, + onOpenConfigureTracing = ::onOpenConfigureTracing, onBackPressed = ::navigateUp ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 23ea4faf86..684587220b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun DeveloperSettingsView( state: DeveloperSettingsState, onOpenShowkase: () -> Unit, + onOpenConfigureTracing: () -> Unit, onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { @@ -47,6 +48,12 @@ fun DeveloperSettingsView( PreferenceCategory(title = "Feature flags") { FeatureListContent(state) } + PreferenceCategory(title = "Rust SDK") { + PreferenceText( + title = "Configure tracing", + onClick = onOpenConfigureTracing, + ) + } PreferenceCategory(title = "Showkase") { PreferenceText( title = "Open Showkase browser", @@ -109,6 +116,7 @@ private fun ContentToPreview(state: DeveloperSettingsState) { DeveloperSettingsView( state = state, onOpenShowkase = {}, + onOpenConfigureTracing = {}, onBackPressed = {} ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingEvents.kt new file mode 100644 index 0000000000..2d73ea0726 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.preferences.impl.developer.tracing + +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target + +sealed interface ConfigureTracingEvents { + data class UpdateFilter(val target: Target, val logLevel: LogLevel) : ConfigureTracingEvents + data object ResetFilters : ConfigureTracingEvents +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingNode.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingNode.kt new file mode 100644 index 0000000000..7c58058798 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingNode.kt @@ -0,0 +1,45 @@ +/* + * 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.preferences.impl.developer.tracing + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class ConfigureTracingNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ConfigureTracingPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ConfigureTracingView( + state = state, + onBackPressed = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt new file mode 100644 index 0000000000..6ad0529597 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt @@ -0,0 +1,53 @@ +/* + * 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.preferences.impl.developer.tracing + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class ConfigureTracingPresenter @Inject constructor( + private val tracingConfigurationStore: TracingConfigurationStore, + private val targetLogLevelMapBuilder: TargetLogLevelMapBuilder, +) : Presenter { + + @Composable + override fun present(): ConfigureTracingState { + val modifiedMap = remember { mutableStateOf(targetLogLevelMapBuilder.getCurrentMap()) } + + fun handleEvents(event: ConfigureTracingEvents) { + when (event) { + is ConfigureTracingEvents.UpdateFilter -> { + modifiedMap.value = modifiedMap.value.toMutableMap() + .apply { this[event.target] = event.logLevel } + tracingConfigurationStore.storeLogLevel(event.target, event.logLevel) + } + ConfigureTracingEvents.ResetFilters -> { + modifiedMap.value = targetLogLevelMapBuilder.getDefaultMap() + tracingConfigurationStore.reset() + } + } + } + + return ConfigureTracingState( + targetsToLogLevel = modifiedMap.value, + eventSink = ::handleEvents + ) + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt new file mode 100644 index 0000000000..f8433a68fb --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt @@ -0,0 +1,25 @@ +/* + * 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.preferences.impl.developer.tracing + +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target + +data class ConfigureTracingState( + val targetsToLogLevel: Map, + val eventSink: (ConfigureTracingEvents) -> Unit +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt new file mode 100644 index 0000000000..06bacb8e74 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.preferences.impl.developer.tracing + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target + +open class ConfigureTracingStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aConfigureTracingState(), + ) +} + +fun aConfigureTracingState() = ConfigureTracingState( + targetsToLogLevel = mapOf( + Target.COMMON to LogLevel.INFO, + Target.MATRIX_SDK_FFI to LogLevel.WARN, + Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.ERROR, + ), + eventSink = {} +) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt new file mode 100644 index 0000000000..bf311e7e22 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingView.kt @@ -0,0 +1,247 @@ +/* + * 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.preferences.impl.developer.tracing + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.list.ListItemContent +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.aliasScreenTitle +import io.element.android.libraries.designsystem.theme.components.DropdownMenu +import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.ListItem +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target +import io.element.android.libraries.theme.ElementTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfigureTracingView( + state: ConfigureTracingState, + onBackPressed: () -> Unit, + modifier: Modifier = Modifier, +) { + var showMenu by remember { mutableStateOf(false) } + Scaffold( + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + contentWindowInsets = WindowInsets.statusBars, + topBar = { + TopAppBar( + modifier = modifier, + navigationIcon = { + BackButton(onClick = onBackPressed) + }, + title = { + Text( + text = "Configure tracing", + style = ElementTheme.typography.aliasScreenTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + actions = { + IconButton( + onClick = { showMenu = !showMenu } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + tint = ElementTheme.materialColors.secondary, + contentDescription = null, + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + onClick = { + showMenu = false + state.eventSink.invoke(ConfigureTracingEvents.ResetFilters) + }, + text = { Text("Reset to default") }, + leadingIcon = { + Icon( + Icons.Outlined.Delete, + tint = ElementTheme.materialColors.secondary, + contentDescription = null, + ) + } + ) + } + } + ) + }, + content = { + Column( + modifier = Modifier + .padding(it) + .consumeWindowInsets(it) + .verticalScroll(state = rememberScrollState()) + ) { + CrateListContent(state) + ListItem( + headlineContent = { + Text( + text = "Kill and restart the app for the change to take effect.", + style = ElementTheme.typography.fontHeadingSmMedium, + ) + }, + ) + } + } + ) +} + +@Composable +fun CrateListContent( + state: ConfigureTracingState, + modifier: Modifier = Modifier +) { + fun onLogLevelChange(target: Target, logLevel: LogLevel) { + state.eventSink(ConfigureTracingEvents.UpdateFilter(target, logLevel)) + } + + TargetAndLogLevelListView( + modifier = modifier, + data = state.targetsToLogLevel, + onLogLevelChange = ::onLogLevelChange, + ) +} + +@Composable +private fun TargetAndLogLevelListView( + data: Map, + onLogLevelChange: (Target, LogLevel) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + data.forEach { item -> + fun onLogLevelChange(logLevel: LogLevel) { + onLogLevelChange(item.key, logLevel) + } + + TargetAndLogLevelView( + target = item.key, + logLevel = item.value, + onLogLevelChange = ::onLogLevelChange + ) + } + } +} + +@Composable +fun TargetAndLogLevelView( + target: Target, + logLevel: LogLevel, + onLogLevelChange: (LogLevel) -> Unit, + modifier: Modifier = Modifier +) { + ListItem( + modifier = modifier, + headlineContent = { Text(text = target.filter.takeIf { it.isNotEmpty() } ?: "(common)") }, + trailingContent = ListItemContent.Custom { + LogLevelDropdownMenu( + logLevel = logLevel, + onLogLevelChange = onLogLevelChange, + ) + }, + ) +} + +@Composable +fun LogLevelDropdownMenu( + logLevel: LogLevel, + onLogLevelChange: (LogLevel) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + DropdownMenuItem( + modifier = modifier.widthIn(max = 120.dp), + text = { Text(text = logLevel.filter) }, + onClick = { expanded = !expanded }, + trailingIcon = { + if (expanded) { + Icon(Icons.Default.ArrowDropUp, contentDescription = null) + } else { + Icon(Icons.Default.ArrowDropDown, contentDescription = null) + } + }, + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + LogLevel.values().forEach { logLevel -> + DropdownMenuItem( + text = { + Text(text = logLevel.filter) + }, + onClick = { + expanded = false + onLogLevelChange(logLevel) + } + ) + } + } +} + +@DayNightPreviews +@Composable +internal fun ConfigureTracingViewPreview( + @PreviewParameter(ConfigureTracingStateProvider::class) state: ConfigureTracingState +) = ElementPreview { + ConfigureTracingView( + state = state, + onBackPressed = {}, + ) +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt new file mode 100644 index 0000000000..c70d573430 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TargetLogLevelMapBuilder.kt @@ -0,0 +1,43 @@ +/* + * 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.preferences.impl.developer.tracing + +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target +import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations +import javax.inject.Inject + +class TargetLogLevelMapBuilder @Inject constructor( + private val tracingConfigurationStore: TracingConfigurationStore, +) { + private val defaultConfig = TracingFilterConfigurations.debug + + fun getDefaultMap(): Map { + return Target.entries.associateWith { target -> + defaultConfig.getLogLevel(target) + ?: LogLevel.INFO + } + } + + fun getCurrentMap(): Map { + return Target.entries.associateWith { target -> + tracingConfigurationStore.getLogLevel(target) + ?: defaultConfig.getLogLevel(target) + ?: LogLevel.INFO + } + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TracingConfigurationStore.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TracingConfigurationStore.kt new file mode 100644 index 0000000000..582231eb50 --- /dev/null +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/TracingConfigurationStore.kt @@ -0,0 +1,60 @@ +/* + * 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.preferences.impl.developer.tracing + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.DefaultPreferences +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target +import javax.inject.Inject + +interface TracingConfigurationStore { + fun getLogLevel(target: Target): LogLevel? + fun storeLogLevel(target: Target, logLevel: LogLevel) + fun reset() +} + +@ContributesBinding(AppScope::class) +class SharedPrefTracingConfigurationStore @Inject constructor( + @DefaultPreferences private val sharedPreferences: SharedPreferences +) : TracingConfigurationStore { + override fun getLogLevel(target: Target): LogLevel? { + return sharedPreferences.getString("$KEY_PREFIX${target.name}", null) + ?.let { LogLevel.valueOf(it) } + } + + override fun storeLogLevel(target: Target, logLevel: LogLevel) { + sharedPreferences.edit { + putString("$KEY_PREFIX${target.name}", logLevel.name) + } + } + + override fun reset() { + sharedPreferences.edit { + sharedPreferences.all.keys.filter { it.startsWith(KEY_PREFIX) }.forEach { + remove(it) + } + } + } + + companion object { + private const val KEY_PREFIX = "tracing_log_level_" + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt new file mode 100644 index 0000000000..979713427e --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenterTest.kt @@ -0,0 +1,104 @@ +/* + * 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.preferences.impl.developer.tracing + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target +import io.element.android.tests.testutils.waitForPredicate +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ConfigureTracingPresenterTest { + @Test + fun `present - initial state`() = runTest { + val store = InMemoryTracingConfigurationStore() + val presenter = ConfigureTracingPresenter( + store, + TargetLogLevelMapBuilder(store), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.targetsToLogLevel).isNotEmpty() + assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG) + } + } + + @Test + fun `present - store is taken into account`() = runTest { + val store = InMemoryTracingConfigurationStore() + store.givenLogLevel(LogLevel.ERROR) + val presenter = ConfigureTracingPresenter( + store, + TargetLogLevelMapBuilder(store), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.targetsToLogLevel).isNotEmpty() + assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.ERROR) + } + } + + @Test + fun `present - change a value`() = runTest { + val store = InMemoryTracingConfigurationStore() + val presenter = ConfigureTracingPresenter( + store, + TargetLogLevelMapBuilder(store), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG) + initialState.eventSink.invoke(ConfigureTracingEvents.UpdateFilter(Target.MATRIX_SDK_CRYPTO, LogLevel.WARN)) + val finalState = awaitItem() + assertThat(finalState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.WARN) + waitForPredicate { store.hasStoreLogLevelBeenCalled } + } + } + + @Test + fun `present - reset`() = runTest { + val store = InMemoryTracingConfigurationStore() + val presenter = ConfigureTracingPresenter( + store, + TargetLogLevelMapBuilder(store), + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG) + initialState.eventSink.invoke(ConfigureTracingEvents.UpdateFilter(Target.MATRIX_SDK_CRYPTO, LogLevel.WARN)) + val finalState = awaitItem() + assertThat(finalState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.WARN) + waitForPredicate { store.hasStoreLogLevelBeenCalled } + finalState.eventSink.invoke(ConfigureTracingEvents.ResetFilters) + val resetState = awaitItem() + assertThat(resetState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG) + waitForPredicate { store.hasResetBeenCalled } + } + } +} diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/InMemoryTracingConfigurationStore.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/InMemoryTracingConfigurationStore.kt new file mode 100644 index 0000000000..1d17a1227b --- /dev/null +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/tracing/InMemoryTracingConfigurationStore.kt @@ -0,0 +1,44 @@ +/* + * 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.preferences.impl.developer.tracing + +import io.element.android.libraries.matrix.api.tracing.LogLevel +import io.element.android.libraries.matrix.api.tracing.Target + +class InMemoryTracingConfigurationStore : TracingConfigurationStore { + var hasResetBeenCalled = false + private set + var hasStoreLogLevelBeenCalled = false + private set + private var logLevel: LogLevel? = null + + fun givenLogLevel(logLevel: LogLevel?) { + this.logLevel = logLevel + } + + override fun getLogLevel(target: Target): LogLevel? { + return logLevel + } + + override fun storeLogLevel(target: Target, logLevel: LogLevel) { + hasStoreLogLevelBeenCalled = true + } + + override fun reset() { + hasResetBeenCalled = true + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt index 596b611296..d34911644e 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/tracing/TracingFilterConfiguration.kt @@ -21,22 +21,27 @@ data class TracingFilterConfiguration( ) { // Order should matters - private val targetsToLogLevel: MutableMap = mutableMapOf( - Target.COMMON to LogLevel.Info, - Target.HYPER to LogLevel.Warn, - Target.MATRIX_SDK_CRYPTO to LogLevel.Debug, - Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.Debug, - Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.Trace, - Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.Trace, - Target.MATRIX_SDK_UI_TIMELINE to LogLevel.Info, + private val targetsToLogLevel: Map = mapOf( + Target.COMMON to LogLevel.INFO, + Target.HYPER to LogLevel.WARN, + Target.MATRIX_SDK_CRYPTO to LogLevel.DEBUG, + Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.DEBUG, + Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.TRACE, + Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.TRACE, + Target.MATRIX_SDK_UI_TIMELINE to LogLevel.INFO, ) + fun getLogLevel(target: Target): LogLevel? { + return overrides[target] ?: targetsToLogLevel[target] + } + val filter: String get() { + val fullMap = targetsToLogLevel.toMutableMap() overrides.forEach { (target, logLevel) -> - targetsToLogLevel[target] = logLevel + fullMap[target] = logLevel } - return targetsToLogLevel.map { + return fullMap.map { if (it.key.filter.isEmpty()) { it.value.filter } else { @@ -59,25 +64,25 @@ enum class Target(open val filter: String) { MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"), } -sealed class LogLevel(val filter: String) { - data object Warn : LogLevel("warn") - data object Trace : LogLevel("trace") - data object Info : LogLevel("info") - data object Debug : LogLevel("debug") - data object Error : LogLevel("error") +enum class LogLevel(open val filter: String) { + ERROR("error"), + WARN("warn"), + INFO("info"), + DEBUG("debug"), + TRACE("trace"), } object TracingFilterConfigurations { val release = TracingFilterConfiguration( overrides = mapOf( - Target.COMMON to LogLevel.Info, - Target.ELEMENT to LogLevel.Debug + Target.COMMON to LogLevel.INFO, + Target.ELEMENT to LogLevel.DEBUG ), ) val debug = TracingFilterConfiguration( overrides = mapOf( - Target.COMMON to LogLevel.Info, - Target.ELEMENT to LogLevel.Trace + Target.COMMON to LogLevel.INFO, + Target.ELEMENT to LogLevel.TRACE ) ) From 55f3b935c36663af084f3c2bfb59575ea258d150 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 1 Sep 2023 14:39:03 +0000 Subject: [PATCH 66/75] Update screenshots --- ...null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ull_DeveloperSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ull_DeveloperSettingsViewDark_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ll_DeveloperSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ll_DeveloperSettingsViewLight_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 6 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..acf8d934bb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-D-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e55b03652dc0149cdc2623fa81b678a68a8080f57043c1f71cc99565e44c98e0 +size 35025 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2a8c0680e3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer.tracing_null_ConfigureTracingView-N-0_2_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e38e363ecdec4e70699204a83a01ac3e4a840f20f1ebc84bd014b49e0e52c77b +size 31291 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png index 29125d81e3..9dbdf12053 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ecff1b486e8aca39a5b7c81139e1132829d9baded8888977eeaa88fd1a6e5f2 -size 49665 +oid sha256:e6b1bfbd1d8c29347433e0c8a1037ea219b0935f9a74196f4c96952d144417e9 +size 48845 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_1,NEXUS_5,1.0,en].png index 29125d81e3..9dbdf12053 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ecff1b486e8aca39a5b7c81139e1132829d9baded8888977eeaa88fd1a6e5f2 -size 49665 +oid sha256:e6b1bfbd1d8c29347433e0c8a1037ea219b0935f9a74196f4c96952d144417e9 +size 48845 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png index be5e358d49..c1ebd00c8e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2123c483c9609b3341c762907e1eb5f50052faa3731454ff0953cdb862809c -size 54357 +oid sha256:6814348aebbbb561fe42ae5e7c5ae9bec77cc7838b41adbc5158954b338fb0d1 +size 53755 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_1,NEXUS_5,1.0,en].png index be5e358d49..c1ebd00c8e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.preferences.impl.developer_null_DeveloperSettingsViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2123c483c9609b3341c762907e1eb5f50052faa3731454ff0953cdb862809c -size 54357 +oid sha256:6814348aebbbb561fe42ae5e7c5ae9bec77cc7838b41adbc5158954b338fb0d1 +size 53755 From 2cb82bd97b33505b7d4faac94163174dc41f9cf2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Sep 2023 16:42:27 +0200 Subject: [PATCH 67/75] Update plugin sonarqube to v4.3.1.3277 (#1210) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a890f4f46c..99e61a7ee4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -203,6 +203,6 @@ dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version. dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } paparazzi = "app.cash.paparazzi:1.3.1" -sonarqube = "org.sonarqube:4.2.1.3168" +sonarqube = "org.sonarqube:4.3.1.3277" kover = "org.jetbrains.kotlinx.kover:0.6.1" sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" } From 73b5ce5fe36ddea3ebc536510f14ccfaaf0e88ef Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 1 Sep 2023 17:39:46 +0200 Subject: [PATCH 68/75] Fix detekted issues. --- .../tracing/ConfigureTracingPresenter.kt | 3 +- .../tracing/ConfigureTracingState.kt | 3 +- .../tracing/ConfigureTracingStateProvider.kt | 3 +- .../developer/tracing/ConfigureTracingView.kt | 57 ++++++++++--------- 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt index 6ad0529597..b0d2243c70 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingPresenter.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableMap import javax.inject.Inject class ConfigureTracingPresenter @Inject constructor( @@ -46,7 +47,7 @@ class ConfigureTracingPresenter @Inject constructor( } return ConfigureTracingState( - targetsToLogLevel = modifiedMap.value, + targetsToLogLevel = modifiedMap.value.toImmutableMap(), eventSink = ::handleEvents ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt index f8433a68fb..bc36a6ede3 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingState.kt @@ -18,8 +18,9 @@ package io.element.android.features.preferences.impl.developer.tracing import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.Target +import kotlinx.collections.immutable.ImmutableMap data class ConfigureTracingState( - val targetsToLogLevel: Map, + val targetsToLogLevel: ImmutableMap, val eventSink: (ConfigureTracingEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt index 06bacb8e74..fe2cfa44a9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/tracing/ConfigureTracingStateProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.preferences.impl.developer.tracing import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.api.tracing.LogLevel import io.element.android.libraries.matrix.api.tracing.Target +import kotlinx.collections.immutable.persistentMapOf open class ConfigureTracingStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,7 +29,7 @@ open class ConfigureTracingStateProvider : PreviewParameterProvider, + data: ImmutableMap, onLogLevelChange: (Target, LogLevel) -> Unit, modifier: Modifier = Modifier, ) { @@ -205,32 +206,34 @@ fun LogLevelDropdownMenu( modifier: Modifier = Modifier, ) { var expanded by remember { mutableStateOf(false) } - DropdownMenuItem( - modifier = modifier.widthIn(max = 120.dp), - text = { Text(text = logLevel.filter) }, - onClick = { expanded = !expanded }, - trailingIcon = { - if (expanded) { - Icon(Icons.Default.ArrowDropUp, contentDescription = null) - } else { - Icon(Icons.Default.ArrowDropDown, contentDescription = null) - } - }, - ) - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - LogLevel.values().forEach { logLevel -> - DropdownMenuItem( - text = { - Text(text = logLevel.filter) - }, - onClick = { - expanded = false - onLogLevelChange(logLevel) + Box(modifier = modifier) { + DropdownMenuItem( + modifier = Modifier.widthIn(max = 120.dp), + text = { Text(text = logLevel.filter) }, + onClick = { expanded = !expanded }, + trailingIcon = { + if (expanded) { + Icon(Icons.Default.ArrowDropUp, contentDescription = null) + } else { + Icon(Icons.Default.ArrowDropDown, contentDescription = null) } - ) + }, + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + LogLevel.values().forEach { logLevel -> + DropdownMenuItem( + text = { + Text(text = logLevel.filter) + }, + onClick = { + expanded = false + onLogLevelChange(logLevel) + } + ) + } } } } From bf3d803db9e338e203cfefa311c9f331c25c5f32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 09:15:14 +0000 Subject: [PATCH 69/75] Update dependency io.element.android:emojibase-bindings to v1.1.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99e61a7ee4..841e53b75d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -167,7 +167,7 @@ sentry = "io.sentry:sentry-android:6.28.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:42b2faa417c1e95f430bf8f6e379adba25ad5ef8" # Emojibase -matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.0.5" +matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.1.3" # Di inject = "javax.inject:javax.inject:1" From cd88be67aeab8c31f5ea1adbe480ca007c88aa0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 4 Sep 2023 11:41:22 +0200 Subject: [PATCH 70/75] Revert exception in proguard --- app/proguard-rules.pro | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e75bf28da3..949ccfce40 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -32,6 +32,3 @@ -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** -dontwarn org.openjsse.** - -# Moshi from EmojiBase --dontwarn com.google.devtools.ksp.processing.SymbolProcessorProvider From 82f6f358a7987e46a38189e78f96455b94f2b6ad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Sep 2023 12:50:37 +0200 Subject: [PATCH 71/75] Ensure the sync is started when receiving a Push, to ensure that the encryption loop is running. Fixes notification with endecrypted content (#1178) --- .../android/appnav/LoggedInFlowNode.kt | 5 ++- .../matrix/api/sync/StartSyncReason.kt | 25 ++++++++++++ .../libraries/matrix/api/sync/SyncService.kt | 4 +- .../matrix/impl/sync/RustSyncService.kt | 40 ++++++++++++++----- .../matrix/test/sync/FakeSyncService.kt | 5 ++- .../notifications/NotifiableEventResolver.kt | 12 ++++++ 6 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 5006218bda..c3a79acb81 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -60,6 +60,7 @@ import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.MAIN_SPACE import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.services.appnavstate.api.AppNavigationStateService @@ -124,7 +125,7 @@ class LoggedInFlowNode @AssistedInject constructor( onStop = { //Counterpart startSync is done in observeSyncStateAndNetworkStatus method. coroutineScope.launch { - syncService.stopSync() + syncService.stopSync(StartSyncReason.AppInForeground) } }, onDestroy = { @@ -150,7 +151,7 @@ class LoggedInFlowNode @AssistedInject constructor( .collect { (syncState, networkStatus) -> Timber.d("Sync state: $syncState, network status: $networkStatus") if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) { - syncService.startSync() + syncService.startSync(StartSyncReason.AppInForeground) } } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt new file mode 100644 index 0000000000..4a6b44949e --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/StartSyncReason.kt @@ -0,0 +1,25 @@ +/* + * 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.libraries.matrix.api.sync + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface StartSyncReason { + data object AppInForeground : StartSyncReason + data class Notification(val roomId: RoomId, val eventId: EventId) : StartSyncReason +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt index 994b35edc4..03a8402b32 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/sync/SyncService.kt @@ -22,12 +22,12 @@ interface SyncService { /** * Tries to start the sync. If already syncing it has no effect. */ - suspend fun startSync(): Result + suspend fun startSync(reason: StartSyncReason): Result /** * Tries to stop the sync. If service is not syncing it has no effect. */ - suspend fun stopSync(): Result + suspend fun stopSync(reason: StartSyncReason): Result /** * Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes. diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt index 932da42afb..b0a9fb31ec 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/RustSyncService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.impl.sync +import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.CoroutineScope @@ -25,6 +26,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.matrix.rustcomponents.sdk.SyncServiceInterface import org.matrix.rustcomponents.sdk.SyncServiceState import timber.log.Timber @@ -33,19 +36,36 @@ class RustSyncService( private val innerSyncService: SyncServiceInterface, sessionCoroutineScope: CoroutineScope ) : SyncService { + private val mutex = Mutex() + private val startSyncReasonSet = mutableSetOf() - override suspend fun startSync() = runCatching { - Timber.i("Start sync") - innerSyncService.start() - }.onFailure { - Timber.d("Start sync failed: $it") + override suspend fun startSync(reason: StartSyncReason): Result { + return mutex.withLock { + startSyncReasonSet.add(reason) + runCatching { + Timber.d("Start sync") + innerSyncService.start() + }.onFailure { + Timber.e("Start sync failed: $it") + } + } } - override suspend fun stopSync() = runCatching { - Timber.i("Stop sync") - innerSyncService.stop() - }.onFailure { - Timber.d("Stop sync failed: $it") + override suspend fun stopSync(reason: StartSyncReason): Result { + return mutex.withLock { + startSyncReasonSet.remove(reason) + if (startSyncReasonSet.isEmpty()) { + runCatching { + Timber.d("Stop sync") + innerSyncService.stop() + }.onFailure { + Timber.e("Stop sync failed: $it") + } + } else { + Timber.d("Stop sync skipped, still $startSyncReasonSet") + Result.success(Unit) + } + } } override val syncState: StateFlow = diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt index 4e618deb9a..87a54a2571 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/sync/FakeSyncService.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.test.sync +import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncState import kotlinx.coroutines.flow.MutableStateFlow @@ -29,12 +30,12 @@ class FakeSyncService : SyncService { syncStateFlow.value = SyncState.Error } - override suspend fun startSync(): Result { + override suspend fun startSync(reason: StartSyncReason): Result { syncStateFlow.value = SyncState.Running return Result.success(Unit) } - override suspend fun stopSync(): Result { + override suspend fun stopSync(reason: StartSyncReason): Result { syncStateFlow.value = SyncState.Terminated return Result.success(Unit) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index e5af7785db..ce3d09f013 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.room.RoomMembershipState +import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType @@ -44,6 +45,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.services.toolbox.api.strings.StringProvider import io.element.android.services.toolbox.api.systemclock.SystemClock +import kotlinx.coroutines.delay import timber.log.Timber import javax.inject.Inject @@ -65,6 +67,14 @@ class NotifiableEventResolver @Inject constructor( // Restore session val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val notificationService = client.notificationService() + + // Restart the sync service to ensure that the crypto sync handle the toDevice Events. + client.syncService().startSync(StartSyncReason.Notification(roomId, eventId)) + // Wait for toDevice Event to be processed + // FIXME This delay can be removed when the Rust SDK will handle internal retry to get + // clear notification content. + delay(300) + val notificationData = notificationService.getNotification( userId = sessionId, roomId = roomId, @@ -73,6 +83,8 @@ class NotifiableEventResolver @Inject constructor( Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.") }.getOrNull() + client.syncService().stopSync(StartSyncReason.Notification(roomId, eventId)) + // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event return notificationData?.asNotifiableEvent(sessionId) } From 948844a901b5fef198cbbf857cfab957ad7fa350 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Sep 2023 12:53:24 +0200 Subject: [PATCH 72/75] changelog. --- changelog.d/1178.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1178.bugfix diff --git a/changelog.d/1178.bugfix b/changelog.d/1178.bugfix new file mode 100644 index 0000000000..2268ca8561 --- /dev/null +++ b/changelog.d/1178.bugfix @@ -0,0 +1 @@ +Ensure notification for Event from encrypted room get decrypted content. From 4766e0cc8f7b897a39284087da8dad5c4f267a85 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Sep 2023 14:23:03 +0200 Subject: [PATCH 73/75] Fix compilation of sample app. --- .../io/element/android/samples/minimal/RoomListScreen.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index faaccc9b8e..bb90425a48 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.eventformatter.impl.StateContentFormatter import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.sync.StartSyncReason import io.element.android.services.toolbox.impl.strings.AndroidStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -109,12 +110,12 @@ class RoomListScreen( DisposableEffect(Unit) { Timber.w("Start sync!") runBlocking { - matrixClient.syncService().startSync() + matrixClient.syncService().startSync(StartSyncReason.AppInForeground) } onDispose { Timber.w("Stop sync!") runBlocking { - matrixClient.syncService().stopSync() + matrixClient.syncService().stopSync(StartSyncReason.AppInForeground) } } } From e15430e9b666b2739fc215315fe4037dd8f3101d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Sep 2023 16:13:27 +0200 Subject: [PATCH 74/75] Changelog for version 0.1.6 --- CHANGES.md | 23 +++++++++++++++++++++++ changelog.d/1135.bugfix | 1 - changelog.d/1143.feature | 1 - changelog.d/1168.bugfix | 1 - changelog.d/1177.bugfix | 1 - changelog.d/1178.bugfix | 1 - changelog.d/1187.misc | 1 - changelog.d/1196.feature | 1 - changelog.d/1198.bugfix | 1 - changelog.d/1995.bugfix | 1 - changelog.d/928.bugfix | 1 - 11 files changed, 23 insertions(+), 10 deletions(-) delete mode 100644 changelog.d/1135.bugfix delete mode 100644 changelog.d/1143.feature delete mode 100644 changelog.d/1168.bugfix delete mode 100644 changelog.d/1177.bugfix delete mode 100644 changelog.d/1178.bugfix delete mode 100644 changelog.d/1187.misc delete mode 100644 changelog.d/1196.feature delete mode 100644 changelog.d/1198.bugfix delete mode 100644 changelog.d/1995.bugfix delete mode 100644 changelog.d/928.bugfix diff --git a/CHANGES.md b/CHANGES.md index c9e3ad2d33..4cca4c7212 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,26 @@ +Changes in Element X v0.1.6 (2023-09-04) +======================================== + +Features ✨ +---------- + - Enable the Polls feature. Allows to create, view, vote and end polls. ([#1196](https://github.com/vector-im/element-x-android/issues/1196)) +- Create poll. ([#1143](https://github.com/vector-im/element-x-android/issues/1143)) + +Bugfixes 🐛 +---------- +- Ensure notification for Event from encrypted room get decrypted content. ([#1178](https://github.com/vector-im/element-x-android/issues/1178)) + - Make sure Snackbars are only displayed once. ([#928](https://github.com/vector-im/element-x-android/issues/928)) + - Fix the orientation of sent images. ([#1135](https://github.com/vector-im/element-x-android/issues/1135)) + - Bug reporter crashes when 'send logs' is disabled. ([#1168](https://github.com/vector-im/element-x-android/issues/1168)) + - Add missing link to the terms on the analytics setting screen. ([#1177](https://github.com/vector-im/element-x-android/issues/1177)) + - Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications. ([#1198](https://github.com/vector-im/element-x-android/issues/1198)) + - Crash with `aspectRatio` modifier when `Float.NaN` was used as input. ([#1995](https://github.com/vector-im/element-x-android/issues/1995)) + +Other changes +------------- + - Remove unnecessary year in copyright mention. ([#1187](https://github.com/vector-im/element-x-android/issues/1187)) + + Changes in Element X v0.1.5 (2023-08-28) ======================================== diff --git a/changelog.d/1135.bugfix b/changelog.d/1135.bugfix deleted file mode 100644 index 2b963c7732..0000000000 --- a/changelog.d/1135.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the orientation of sent images. diff --git a/changelog.d/1143.feature b/changelog.d/1143.feature deleted file mode 100644 index 84a86f4f25..0000000000 --- a/changelog.d/1143.feature +++ /dev/null @@ -1 +0,0 @@ -Create poll. diff --git a/changelog.d/1168.bugfix b/changelog.d/1168.bugfix deleted file mode 100644 index f7f959ac0a..0000000000 --- a/changelog.d/1168.bugfix +++ /dev/null @@ -1 +0,0 @@ -Bug reporter crashes when 'send logs' is disabled. diff --git a/changelog.d/1177.bugfix b/changelog.d/1177.bugfix deleted file mode 100644 index edbf2e9006..0000000000 --- a/changelog.d/1177.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add missing link to the terms on the analytics setting screen. diff --git a/changelog.d/1178.bugfix b/changelog.d/1178.bugfix deleted file mode 100644 index 2268ca8561..0000000000 --- a/changelog.d/1178.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ensure notification for Event from encrypted room get decrypted content. diff --git a/changelog.d/1187.misc b/changelog.d/1187.misc deleted file mode 100644 index 301e3a6fc4..0000000000 --- a/changelog.d/1187.misc +++ /dev/null @@ -1 +0,0 @@ -Remove unnecessary year in copyright mention. diff --git a/changelog.d/1196.feature b/changelog.d/1196.feature deleted file mode 100644 index fe1eb354f9..0000000000 --- a/changelog.d/1196.feature +++ /dev/null @@ -1 +0,0 @@ -Enable the Polls feature. Allows to create, view, vote and end polls. diff --git a/changelog.d/1198.bugfix b/changelog.d/1198.bugfix deleted file mode 100644 index 6ef69c4eff..0000000000 --- a/changelog.d/1198.bugfix +++ /dev/null @@ -1 +0,0 @@ -Re-enable `SyncService.withEncryptionSync` to improve decryption of notifications. diff --git a/changelog.d/1995.bugfix b/changelog.d/1995.bugfix deleted file mode 100644 index acec25add0..0000000000 --- a/changelog.d/1995.bugfix +++ /dev/null @@ -1 +0,0 @@ -Crash with `aspectRatio` modifier when `Float.NaN` was used as input. diff --git a/changelog.d/928.bugfix b/changelog.d/928.bugfix deleted file mode 100644 index 98a4cd34e0..0000000000 --- a/changelog.d/928.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make sure Snackbars are only displayed once. From fa8979126961c28947ac000ff72ddb1e210370f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 4 Sep 2023 16:13:48 +0200 Subject: [PATCH 75/75] Adding fastlane file for version 0.1.6 --- fastlane/metadata/android/en-US/changelogs/40001060.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/en-US/changelogs/40001060.txt diff --git a/fastlane/metadata/android/en-US/changelogs/40001060.txt b/fastlane/metadata/android/en-US/changelogs/40001060.txt new file mode 100644 index 0000000000..ff4f86ce7e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40001060.txt @@ -0,0 +1,2 @@ +Main changes in this version: bugfixes. +Full changelog: https://github.com/vector-im/element-x-android/releases