From a73378b0cec9aafd7b3abe0ea8d0bf765a805f07 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 08:51:21 +0200 Subject: [PATCH 1/7] Update dependency androidx.recyclerview:recyclerview to v1.3.2 (#1599) 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 9a0bfc3a04..dd711b6d72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ core = "1.12.0" datastore = "1.0.0" constraintlayout = "2.1.4" constraintlayout_compose = "1.0.1" -recyclerview = "1.3.1" +recyclerview = "1.3.2" lifecycle = "2.6.2" activity = "1.8.0" startup = "1.1.1" From 6bec6235b4ce7a2eddb4a0c866463a2b507ab36d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 10:48:23 +0200 Subject: [PATCH 2/7] Update dependency io.sentry:sentry-android to v6.32.0 (#1602) 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 dd711b6d72..b2e91fce99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -167,7 +167,7 @@ maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.1" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry = "io.sentry:sentry-android:6.31.0" +sentry = "io.sentry:sentry-android:6.32.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:e9cd9adaf18cec52ed851395eb84358b4f9b8d7f" # Emojibase From 9aa4c595165c1b1c9984aad3754d33a566861c3d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 19 Oct 2023 10:49:11 +0200 Subject: [PATCH 3/7] Hide keyboard when exiting the room screen (#1593) --- changelog.d/1375.bugfix | 1 + .../android/features/messages/impl/MessagesView.kt | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/1375.bugfix diff --git a/changelog.d/1375.bugfix b/changelog.d/1375.bugfix new file mode 100644 index 0000000000..d80ff3543c --- /dev/null +++ b/changelog.d/1375.bugfix @@ -0,0 +1 @@ +Hide keyboard when exiting the chat room screen. 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 971df06053..b79e84a2e0 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 @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -222,6 +223,14 @@ fun MessagesView( ReinviteDialog( state = state ) + + // Since the textfield is now based on an Android view, this is no longer done automatically. + // We need to hide the keyboard automatically when navigating out of this screen. + DisposableEffect(Unit) { + onDispose { + localView.hideKeyboard() + } + } } @Composable From 79d2941fe4b1617895d18dc4e935d724b621e1e4 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 11:00:32 +0200 Subject: [PATCH 4/7] Include desugaring lib also in library modules (#1604) ## Type of change - [ ] Feature - [ ] Bugfix - [x] Technical - [ ] Other : ## Content Includes the `coreLibraryDesugaring(libs.android.desugar)` dependency in all modules which use one of our gradle plugins. ## Motivation and context Right now desugaring is enabled also in library modules but the desugar dependency is not included in those. This causes some unwanted side effects such as being unable to run compose previews in an emu. This change will also include the desugar dependency in those libraries. --- app/build.gradle.kts | 1 - libraries/pushstore/impl/build.gradle.kts | 2 -- libraries/session-storage/impl/build.gradle.kts | 2 -- plugins/src/main/kotlin/extension/CommonExtension.kt | 1 - .../kotlin/io.element.android-compose-application.gradle.kts | 4 ++++ .../main/kotlin/io.element.android-compose-library.gradle.kts | 4 ++++ plugins/src/main/kotlin/io.element.android-library.gradle.kts | 4 ++++ samples/minimal/build.gradle.kts | 1 - 8 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 839a5095dd..6ac84cfec2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,6 @@ dependencies { implementation(projects.appnav) anvil(projects.anvilcodegen) - coreLibraryDesugaring(libs.android.desugar) implementation(libs.appyx.core) implementation(libs.androidx.splash) implementation(libs.androidx.core) diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 5946e77694..17e0268af1 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -55,6 +55,4 @@ dependencies { androidTestImplementation(libs.test.truth) androidTestImplementation(libs.test.runner) androidTestImplementation(projects.libraries.sessionStorage.test) - - coreLibraryDesugaring(libs.android.desugar) } diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index 03de9acf86..cfbfa4c57d 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -45,8 +45,6 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(libs.sqldelight.driver.jvm) - - coreLibraryDesugaring(libs.android.desugar) } sqldelight { diff --git a/plugins/src/main/kotlin/extension/CommonExtension.kt b/plugins/src/main/kotlin/extension/CommonExtension.kt index e3f7b3682e..97305dbc66 100644 --- a/plugins/src/main/kotlin/extension/CommonExtension.kt +++ b/plugins/src/main/kotlin/extension/CommonExtension.kt @@ -31,7 +31,6 @@ fun CommonExtension<*, *, *, *, *>.androidConfig(project: Project) { } compileOptions { - isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } diff --git a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts index af73409888..80bc0f884e 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-application.gradle.kts @@ -32,9 +32,13 @@ plugins { android { androidConfig(project) composeConfig(libs) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) composeDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts index e420ab3c8d..3194505e4e 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts @@ -32,9 +32,13 @@ plugins { android { androidConfig(project) composeConfig(libs) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) composeDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/plugins/src/main/kotlin/io.element.android-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-library.gradle.kts index 6c3c77223c..f3a84031e6 100644 --- a/plugins/src/main/kotlin/io.element.android-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-library.gradle.kts @@ -29,8 +29,12 @@ plugins { android { androidConfig(project) + compileOptions { + isCoreLibraryDesugaringEnabled = true + } } dependencies { commonDependencies(libs) + coreLibraryDesugaring(libs.android.desugar) } diff --git a/samples/minimal/build.gradle.kts b/samples/minimal/build.gradle.kts index 1473bd7f93..016989a2d6 100644 --- a/samples/minimal/build.gradle.kts +++ b/samples/minimal/build.gradle.kts @@ -65,5 +65,4 @@ dependencies { implementation(projects.services.toolbox.impl) implementation(projects.libraries.featureflag.impl) implementation(libs.coroutines.core) - coreLibraryDesugaring(libs.android.desugar) } From 5a7f77bc92c2179281531f55fc7be630976d88c8 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 13:32:43 +0200 Subject: [PATCH 5/7] Extract more content from audio messages. (#1607) `TimelineItemAudioContent`: - Use `java.time.Duration` instead of milliseconds. This will ease up things in the future because currently milliseconds are sent over the wire but in the future seconds will be sent (as per the stable MSC). Using `Duration` will allow our downstream code to be independent of what's passed over the wire. - Rename `audioSource` property to `mediaSource` to better match its type. `AudioMessageType`: - Add and populate new fields `details` and `isVoiceMessage` to be used by voice messages. --- .../messages/impl/MessagesFlowNode.kt | 2 +- .../TimelineItemContentMessageFactory.kt | 7 +++-- .../model/event/TimelineItemAudioContent.kt | 5 ++-- .../event/TimelineItemAudioContentProvider.kt | 5 ++-- .../DefaultRoomLastMessageFormatterTest.kt | 2 +- .../matrix/api/media/AudioDetails.kt | 24 +++++++++++++++ .../api/timeline/item/event/MessageType.kt | 5 +++- .../matrix/impl/media/AudioDetails.kt | 30 +++++++++++++++++++ .../timeline/item/event/EventMessageMapper.kt | 8 ++++- 9 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt 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 fcb2e7e5e8..21e384906e 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 @@ -238,7 +238,7 @@ class MessagesFlowNode @AssistedInject constructor( backstack.push(navTarget) } is TimelineItemAudioContent -> { - val mediaSource = event.content.audioSource + val mediaSource = event.content.mediaSource val navTarget = NavTarget.MediaViewer( mediaInfo = MediaInfo( name = event.content.body, 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 ae2ea4f350..323f110f47 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 @@ -40,6 +40,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import java.time.Duration import javax.inject.Inject class TimelineItemContentMessageFactory @Inject constructor( @@ -103,11 +104,11 @@ class TimelineItemContentMessageFactory @Inject constructor( } is AudioMessageType -> TimelineItemAudioContent( body = messageType.body, - audioSource = messageType.source, - duration = messageType.info?.duration?.toMillis() ?: 0L, + mediaSource = messageType.source, + duration = messageType.info?.duration ?: Duration.ZERO, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), - fileExtension = fileExtensionExtractor.extractFromName(messageType.body) + fileExtension = fileExtensionExtractor.extractFromName(messageType.body), ) is FileMessageType -> { val fileExtension = fileExtensionExtractor.extractFromName(messageType.body) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt index 485b863170..9d9a41e0e3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt @@ -18,11 +18,12 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize import io.element.android.libraries.matrix.api.media.MediaSource +import java.time.Duration data class TimelineItemAudioContent( val body: String, - val duration: Long, - val audioSource: MediaSource, + val duration: Duration, + val mediaSource: MediaSource, val mimeType: String, val formattedFileSize: String, val fileExtension: String, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt index ed424781f8..06cb53b6fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource +import java.time.Duration open class TimelineItemAudioContentProvider : PreviewParameterProvider { override val values: Sequence @@ -34,6 +35,6 @@ fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAu mimeType = MimeTypes.Pdf, formattedFileSize = "100kB", fileExtension = "mp3", - duration = 100, - audioSource = MediaSource(""), + duration = Duration.ofMillis(100), + mediaSource = MediaSource(""), ) diff --git a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt index 4c26fcb3c7..50d313f132 100644 --- a/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt +++ b/libraries/eventformatter/impl/src/test/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatterTest.kt @@ -161,7 +161,7 @@ class DefaultRoomLastMessageFormatterTest { val sharedContentMessagesTypes = arrayOf( TextMessageType(body, null), VideoMessageType(body, MediaSource("url"), null), - AudioMessageType(body, MediaSource("url"), null), + AudioMessageType(body, MediaSource("url"), null, null, false), ImageMessageType(body, MediaSource("url"), null), FileMessageType(body, MediaSource("url"), null), LocationMessageType(body, "geo:1,2", null), diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt new file mode 100644 index 0000000000..f8cd2d3fb4 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioDetails.kt @@ -0,0 +1,24 @@ +/* + * 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.media + +import java.time.Duration + +data class AudioDetails( + val duration: Duration, + val waveform: List, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt index dc06d5c94a..ba6eeca819 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/MessageType.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.timeline.item.event +import io.element.android.libraries.matrix.api.media.AudioDetails import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo @@ -46,7 +47,9 @@ data class LocationMessageType( data class AudioMessageType( val body: String, val source: MediaSource, - val info: AudioInfo? + val info: AudioInfo?, + val details: AudioDetails?, + val isVoiceMessage: Boolean, ) : MessageType data class VideoMessageType( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt new file mode 100644 index 0000000000..c3fa11e40c --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioDetails.kt @@ -0,0 +1,30 @@ +/* + * 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.impl.media + +import io.element.android.libraries.matrix.api.media.AudioDetails +import org.matrix.rustcomponents.sdk.UnstableAudioDetailsContent as RustAudioDetails + +fun RustAudioDetails.map(): AudioDetails = AudioDetails( + duration = duration, + waveform = waveform.map { it.toInt() }, +) + +fun AudioDetails.map(): RustAudioDetails = RustAudioDetails( + duration = duration, + waveform = waveform.map { it.toUShort() } +) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 0a59cfddab..18d2e1bdeb 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -75,7 +75,13 @@ class EventMessageMapper { fun mapMessageType(type: RustMessageType?) = when (type) { is RustMessageType.Audio -> { - AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) + AudioMessageType( + body = type.content.body, + source = type.content.source.map(), + info = type.content.info?.map(), + details = type.content.audio?.map(), + isVoiceMessage = type.content.voice != null, + ) } is RustMessageType.File -> { FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) From 33f5c8efb8f1e495d447532b0b0ff6c17bee6099 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 14:07:45 +0200 Subject: [PATCH 6/7] Add global `context.cacheDir` provider. (#1606) ## Type of change - [ ] Feature - [ ] Bugfix - [x] Technical - [ ] Other : ## Content Dagger now provides the app's `cacheDir` when requesting a `@CacheDirectory File` type. ## Motivation and context To support some upcoming code that needs the `cacheDir` to be changed during tests. --- .../io/element/android/x/di/AppModule.kt | 7 +++++ .../libraries/di/ApplicationContext.kt | 8 +++++- .../android/libraries/di/CacheDirectory.kt | 27 +++++++++++++++++++ .../matrix/impl/RustMatrixClientFactory.kt | 7 +++-- .../android/samples/minimal/MainActivity.kt | 2 +- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.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 17ba415762..037cec1e71 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 @@ -31,6 +31,7 @@ import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.CacheDirectory import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.SingleIn import io.element.android.x.BuildConfig @@ -51,6 +52,12 @@ object AppModule { return File(context.filesDir, "sessions") } + @Provides + @CacheDirectory + fun providesCacheDirectory(@ApplicationContext context: Context): File { + return context.cacheDir + } + @Provides fun providesResources(@ApplicationContext context: Context): Resources { return context.resources diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt index 2108678097..421b521192 100644 --- a/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/ApplicationContext.kt @@ -18,4 +18,10 @@ package io.element.android.libraries.di import javax.inject.Qualifier -@Qualifier annotation class ApplicationContext +/** + * Qualifies a [Context] object that represents the application context. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class ApplicationContext diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt new file mode 100644 index 0000000000..e8513bef45 --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/CacheDirectory.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 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.di + +import javax.inject.Qualifier + +/** + * Qualifies a [File] object which represents the application cache directory. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class CacheDirectory 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 b37266342e..b1cb3ffac4 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 @@ -16,9 +16,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.di.CacheDirectory import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -32,8 +31,8 @@ import java.io.File import javax.inject.Inject class RustMatrixClientFactory @Inject constructor( - @ApplicationContext private val context: Context, private val baseDirectory: File, + @CacheDirectory private val cacheDirectory: File, private val appCoroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, @@ -63,7 +62,7 @@ class RustMatrixClientFactory @Inject constructor( appCoroutineScope = appCoroutineScope, dispatchers = coroutineDispatchers, baseDirectory = baseDirectory, - baseCacheDirectory = context.cacheDir, + baseCacheDirectory = cacheDirectory, clock = clock, ) } 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 c95a44d8ea..a405167cbf 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 @@ -49,8 +49,8 @@ class MainActivity : ComponentActivity() { sessionStore = sessionStore, userAgentProvider = userAgentProvider, rustMatrixClientFactory = RustMatrixClientFactory( - context = applicationContext, baseDirectory = baseDirectory, + cacheDirectory = applicationContext.cacheDir, appCoroutineScope = Singleton.appScope, coroutineDispatchers = Singleton.coroutineDispatchers, sessionStore = sessionStore, From a814c4a95ab3cd1148c3cb35cf83eca040ce6c90 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Thu, 19 Oct 2023 15:57:34 +0200 Subject: [PATCH 7/7] TimelineItemPresenterFactories (#1609) DI infrastructure to allow injection of presenters into the timeline. Add an `@AssistedFactory` of type `TimelineItemPresenterFactory` to a `Presenter` class and bind this factory into the TimelineItemPresenterFactory map multi binding using: ``` @Binds @IntoMap @TimelineItemEventContentKey(MyTimelineItemContent::class) ``` A map multibinding of such factories will be available in the `LocalTimelineItemPresenterFactories` composition local for further use down the UI tree. --- .../features/messages/impl/MessagesNode.kt | 32 +++++--- .../di/TimelineItemEventContentKey.kt | 29 +++++++ .../di/TimelineItemPresenterFactories.kt | 77 +++++++++++++++++++ .../di/TimelineItemPresenterFactory.kt | 33 ++++++++ tools/detekt/detekt.yml | 2 +- 5 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt 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 6a3cf502d7..dbf7e2fbb2 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 @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.modality.BuildContext @@ -28,6 +29,8 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories +import io.element.android.features.messages.impl.timeline.di.TimelineItemPresenterFactories import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId @@ -44,6 +47,7 @@ class MessagesNode @AssistedInject constructor( private val room: MatrixRoom, private val analyticsService: AnalyticsService, private val presenterFactory: MessagesPresenter.Factory, + private val timelineItemPresenterFactories: TimelineItemPresenterFactories, ) : Node(buildContext, plugins = plugins), MessagesNavigator { private val presenter = presenterFactory.create(this) @@ -106,17 +110,21 @@ class MessagesNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - val state = presenter.present() - MessagesView( - state = state, - onBackPressed = this::navigateUp, - onRoomDetailsClicked = this::onRoomDetailsClicked, - onEventClicked = this::onEventClicked, - onPreviewAttachments = this::onPreviewAttachments, - onUserDataClicked = this::onUserDataClicked, - onSendLocationClicked = this::onSendLocationClicked, - onCreatePollClicked = this::onCreatePollClicked, - modifier = modifier, - ) + CompositionLocalProvider( + LocalTimelineItemPresenterFactories provides timelineItemPresenterFactories, + ) { + val state = presenter.present() + MessagesView( + state = state, + onBackPressed = this::navigateUp, + onRoomDetailsClicked = this::onRoomDetailsClicked, + onEventClicked = this::onEventClicked, + 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/timeline/di/TimelineItemEventContentKey.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt new file mode 100644 index 0000000000..9cb046a054 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemEventContentKey.kt @@ -0,0 +1,29 @@ +/* + * 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.di + +import dagger.MapKey +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import kotlin.reflect.KClass + +/** + * Annotation to add a factory of type [TimelineItemPresenterFactory] to a + * Dagger map multi binding keyed with a subclass of [TimelineItemEventContent]. + */ +@Retention(AnnotationRetention.RUNTIME) +@MapKey +annotation class TimelineItemEventContentKey(val value: KClass) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt new file mode 100644 index 0000000000..0574f7e903 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactories.kt @@ -0,0 +1,77 @@ +/* + * 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.di + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.multibindings.Multibinds +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.RoomScope +import javax.inject.Inject + +/** + * Dagger module that declares the [TimelineItemPresenterFactory] map multi binding. + * + * Its sole purpose is to support the case of an empty map multibinding. + */ +@Module +@ContributesTo(RoomScope::class) +interface TimelineItemPresenterFactoriesModule { + @Multibinds + fun multiBindTimelineItemPresenterFactories(): @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>> +} + +/** + * Wrapper around the [TimelineItemPresenterFactory] map multi binding. + * + * Its only purpose is to provide a nicer type name than: + * `@JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>`. + * + * A typealias would have been better but typealiases on Dagger types which use @JvmSuppressWildcards + * currently make Dagger crash. + * + * Request this type from Dagger to access the [TimelineItemPresenterFactory] map multibinding. + */ +data class TimelineItemPresenterFactories @Inject constructor( + val factories: @JvmSuppressWildcards Map, TimelineItemPresenterFactory<*, *>>, +) + +/** + * Provides a [TimelineItemPresenterFactories] to the composition. + */ +val LocalTimelineItemPresenterFactories = staticCompositionLocalOf { + TimelineItemPresenterFactories(emptyMap()) +} + +/** + * Creates and remembers a presenter for the given content. + * + * Will throw if the presenter is not found in the [TimelineItemPresenterFactory] map multi binding. + */ +@Composable +inline fun TimelineItemPresenterFactories.rememberPresenter( + content: C +): Presenter = remember(content) { + factories.getValue(C::class.java).let { + @Suppress("UNCHECKED_CAST") + (it as TimelineItemPresenterFactory).create(content) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt new file mode 100644 index 0000000000..f79d606f60 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/di/TimelineItemPresenterFactory.kt @@ -0,0 +1,33 @@ +/* + * 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.di + +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent +import io.element.android.libraries.architecture.Presenter + +/** + * A factory for a [Presenter] associated with a timeline item. + * + * Implementations should be annotated with [AssistedFactory] to be created by Dagger. + * + * @param C The timeline item's [TimelineItemEventContent] subtype. + * @param S The [Presenter]'s state class. + * @return A [Presenter] that produces a state of type [S] for the given content of type [C]. + */ +fun interface TimelineItemPresenterFactory { + fun create(content: C): Presenter +} diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 1c226dd5ed..ca8370ae23 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -219,7 +219,7 @@ Compose: CompositionLocalAllowlist: active: true # You can optionally define a list of CompositionLocals that are allowed here - allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState + allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState, LocalTimelineItemPresenterFactories CompositionLocalNaming: active: true ContentEmitterReturningValues: