Merge branch 'release/0.4.1' into main

This commit is contained in:
ganfra 2024-01-17 11:21:25 +01:00
commit 8bc39ce1c4
1786 changed files with 7343 additions and 4262 deletions

View file

@ -14,10 +14,19 @@ ij_smart_tabs = false
ij_visual_guides = none ij_visual_guides = none
ij_wrap_on_typing = false ij_wrap_on_typing = false
# Ktlint rule, for more information see https://pinterest.github.io/ktlint/faq/#why-is-editorconfig-property-disabled_rules-deprecated-and-how-do-i-resolve-this # Ktlint rule, for more information see https://pinterest.github.io/ktlint/1.1.1/faq/#how-do-i-enable-or-disable-a-rule
ktlint_standard_wrapping = disabled ktlint_standard_wrapping = disabled
ktlint_standard_trailing-comma-on-call-site = disabled ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_spacing-between-declarations-with-annotations = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_annotation = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_indent = disabled
ktlint_standard_blank-line-before-declaration = disabled
ktlint_function_naming_ignore_when_annotated_with=Composable
[*.java] [*.java]
ij_java_align_consecutive_assignments = false ij_java_align_consecutive_assignments = false

View file

@ -18,12 +18,6 @@
], ],
"groupName" : "kotlin" "groupName" : "kotlin"
}, },
{
"matchPackageNames" : [
"org.jetbrains.kotlinx.kover"
],
"enabled" : false
},
{ {
"matchPackagePatterns" : [ "matchPackagePatterns" : [
"^org.maplibre" "^org.maplibre"

31
.github/workflows/fork-pr-notice.yml vendored Normal file
View file

@ -0,0 +1,31 @@
name: Community PR notice
on:
workflow_dispatch:
pull_request_target:
types:
- opened
- reopened
jobs:
welcome:
runs-on: ubuntu-latest
name: Welcome comment
if: github.event.pull_request.fork != null
steps:
- name: Add auto-generated commit warning
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:
- Your branch should be based on \`origin/develop\`, at least when it was created.
- There is a changelog entry in the \`changelog.d\` folder with [the Towncrier format](https://towncrier.readthedocs.io/en/latest/tutorial.html#creating-news-fragments).
- The test pass locally running \`./gradlew test\`.
- The code quality check suite pass locally running \`./gradlew runQualityChecks\`.
- If you modified anything related to the UI, including previews, you'll have to run the \`Record screenshots\` GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, **it will prevent the CI from running temporarily**, until you upload a new commit after that one. To do so, just pull the latest changes and push [an empty commit](https://coderwall.com/p/vkdekq/git-commit-allow-empty).`
})

View file

@ -24,27 +24,31 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- name: Remove Run-Maestro label - name: Remove Run-Maestro label
if: ${{ github.event.label.name == 'Run-Maestro' }} if: ${{ github.event_name == 'pull_request' && github.event.label.name == 'Run-Maestro' }}
uses: actions-ecosystem/action-remove-labels@v1 uses: actions-ecosystem/action-remove-labels@v1
with: with:
labels: Run-Maestro labels: Run-Maestro
- uses: actions/checkout@v4 - uses: actions/checkout@v4
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@v4 - uses: actions/setup-java@v4
name: Use JDK 17 name: Use JDK 17
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: Assemble debug APK - name: Assemble debug APK
run: ./gradlew :app:assembleDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleDebug $CI_GRADLE_ARG_PROPERTIES
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.0 - uses: mobile-dev-inc/action-maestro-cloud@v1.8.0
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }} api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
# Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android): # Doc says (https://github.com/mobile-dev-inc/action-maestro-cloud#android):

View file

@ -33,7 +33,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈 Generate kover report and verify coverage - name: 📈 Generate kover report and verify coverage
run: ./gradlew koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()
@ -41,7 +41,7 @@ jobs:
with: with:
name: kover-results name: kover-results
path: | path: |
**/build/reports/kover/merged **/build/reports/kover
- name: 🔊 Publish results to Sonar - name: 🔊 Publish results to Sonar
env: env:

View file

@ -26,7 +26,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch) - name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@v1.2.2 uses: nschloe/action-cached-lfs-checkout@v1.2.2
@ -43,8 +43,8 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots
id: record
run: ./.github/workflows/scripts/recordScreenshots.sh run: ./.github/workflows/scripts/recordScreenshots.sh
env: env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}

View file

@ -68,11 +68,27 @@ echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn ./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn
echo "Committing changes" echo "Committing changes"
git config user.name "ElementBot" git config http.sslVerify false
git config user.email "benoitm+elementbot@element.io"
if [[ -z ${INPUT_AUTHOR_NAME} ]]; then
git config user.name "ElementBot"
else
git config --local user.name "${INPUT_AUTHOR_NAME}"
fi
if [[ -z ${INPUT_AUTHOR_EMAIL} ]]; then
git config user.email "benoitm+elementbot@element.io"
else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi
git add -A git add -A
git commit -m "Update screenshots" git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
echo "Pushing changes" echo "Pushing changes"
git push "https://$TOKEN@github.com/$REPO.git" $BRANCH if [[ -z ${GITHUB_ACTOR} ]]; then
echo "No GITHUB_ACTOR env var"
GITHUB_REPO="https://$TOKEN@github.com/$REPO.git"
fi
git push $GITHUB_REPO "$BRANCH"
echo "Done!" echo "Done!"

View file

@ -55,7 +55,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈Generate kover report and verify coverage - name: 📈Generate kover report and verify coverage
run: ./gradlew koverMergedReport koverMergedVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew :app:koverHtmlReport :app:koverXmlReport :app:koverVerify $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
@ -63,7 +63,7 @@ jobs:
with: with:
name: kover-error-report name: kover-error-report
path: | path: |
**/kover/merged/verification/errors.txt app/build/reports/kover/verify.err
- name: ✅ Upload kover report (disabled) - name: ✅ Upload kover report (disabled)
if: always() if: always()
@ -83,4 +83,4 @@ jobs:
if: always() if: always()
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
# with: # with:
# files: build/reports/kover/merged/xml/report.xml # files: build/reports/kover/xml/report.xml

View file

@ -6,12 +6,10 @@ appId: ${APP_ID}
- takeScreenshot: build/maestro/900-SignOutScreen - takeScreenshot: build/maestro/900-SignOutScreen
- back - back
- tapOn: "Sign out" - tapOn: "Sign out"
- tapOn:
id: "sign-out-submit"
# Ensure cancel cancels # Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: - tapOn:
id: "sign-out-submit" id: "dialog-negative"
- tapOn: "Sign out"
- tapOn: - tapOn:
id: "dialog-positive" id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml - runFlow: ../assertions/assertInitDisplayed.yaml

View file

@ -9,5 +9,5 @@ appId: ${APP_ID}
index: 1 index: 1
- takeScreenshot: build/maestro/330-createAndDeleteDM - takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2" - tapOn: "maestroelement2"
- tapOn: "Leave room" - tapOn: "Leave conversation"
- tapOn: "Leave" - tapOn: "Leave"

View file

@ -1,3 +1,39 @@
Changes in Element X v0.4.1 (2024-01-17)
========================================
Features ✨
----------
- Render m.sticker events ([#1949](https://github.com/element-hq/element-x-android/issues/1949))
- Add support for sending images from the keyboard ([#1977](https://github.com/element-hq/element-x-android/issues/1977))
- Added support for MSC4027 (render custom images in reactions) ([#2159](https://github.com/element-hq/element-x-android/issues/2159))
Bugfixes 🐛
----------
- Fix crash sending image with latest Posthog because of an usage of an internal Android method. ([#+crash-sending-image-with-latest-posthog](https://github.com/element-hq/element-x-android/issues/+crash-sending-image-with-latest-posthog))
- Make sure the media viewer tries the main url first (if not empty) then the thumbnail url and then not open if both are missing instead of failing with an error dialog ([#1949](https://github.com/element-hq/element-x-android/issues/1949))
- Fix room transition animation happens twice. ([#2084](https://github.com/element-hq/element-x-android/issues/2084))
- Disable ability to send reaction if the user does not have the permission to. ([#2093](https://github.com/element-hq/element-x-android/issues/2093))
- Trim whitespace at the end of messages to ensure we render the right content. ([#2099](https://github.com/element-hq/element-x-android/issues/2099))
- Fix crashes in room list when the last message for a room was an extremely long one (several thousands of characters) with no line breaks. ([#2105](https://github.com/element-hq/element-x-android/issues/2105))
- Disable rasterisation of Vector XMLs, which was causing crashes on API 23. ([#2124](https://github.com/element-hq/element-x-android/issues/2124))
- Use `SubomposeLayout` for `ContentAvoidingLayout` to prevent wrong measurements in the layout process, leading to cut-off text messages in the timeline. ([#2155](https://github.com/element-hq/element-x-android/issues/2155))
- Improve rendering of voice messages in the timeline in large displays ([#2156](https://github.com/element-hq/element-x-android/issues/2156))
- Fix no indication that user list is loading when inviting to room. ([#2172](https://github.com/element-hq/element-x-android/issues/2172))
- Hide keyboard when tapping on a message in the timeline. ([#2182](https://github.com/element-hq/element-x-android/issues/2182))
- Mention selector gets stuck when quickly deleting the prompt. ([#2192](https://github.com/element-hq/element-x-android/issues/2192))
- Hide verbose state events from the timeline ([#2216](https://github.com/element-hq/element-x-android/issues/2216))
Other changes
-------------
- Only apply `com.autonomousapps.dependency-analysis` plugin in those modules that need it. ([#+only-apply-dependency-analysis-plugin-where-needed](https://github.com/element-hq/element-x-android/issues/+only-apply-dependency-analysis-plugin-where-needed))
- Migrate to Kover 0.7.X ([#1782](https://github.com/element-hq/element-x-android/issues/1782))
- Remove extra logout screen. ([#2072](https://github.com/element-hq/element-x-android/issues/2072))
- Handle `MembershipChange.NONE` rendering in the timeline. ([#2102](https://github.com/element-hq/element-x-android/issues/2102))
- Remove extra previews for timestamp view with 'document' case ([#2127](https://github.com/element-hq/element-x-android/issues/2127))
- Bump AGP version to 8.2.0 ([#2142](https://github.com/element-hq/element-x-android/issues/2142))
- Replace 'leave room' text with 'leave conversation' for DMs. ([#2218](https://github.com/element-hq/element-x-android/issues/2218))
Changes in Element X v0.4.0 (2023-12-22) Changes in Element X v0.4.0 (2023-12-22)
======================================== ========================================

View file

@ -58,7 +58,6 @@ import java.io.File
*/ */
@AutoService(CodeGenerator::class) @AutoService(CodeGenerator::class)
class ContributesNodeCodeGenerator : CodeGenerator { class ContributesNodeCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true override fun isApplicable(context: AnvilContext): Boolean = true
override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> { override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> {

View file

@ -20,6 +20,7 @@ import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import extension.allFeaturesImpl import extension.allFeaturesImpl
import extension.allLibrariesImpl import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins { plugins {
id("io.element.android-compose-application") id("io.element.android-compose-application")
@ -27,7 +28,8 @@ plugins {
alias(libs.plugins.anvil) alias(libs.plugins.anvil)
alias(libs.plugins.ksp) alias(libs.plugins.ksp)
alias(libs.plugins.kapt) alias(libs.plugins.kapt)
alias(libs.plugins.firebaseAppDistribution) // When using precompiled plugins, we need to apply the firebase plugin like this
id(libs.plugins.firebaseAppDistribution.get().pluginId)
alias(libs.plugins.knit) alias(libs.plugins.knit)
id("kotlin-parcelize") id("kotlin-parcelize")
// To be able to update the firebase.xml files, uncomment and build the project // To be able to update the firebase.xml files, uncomment and build the project
@ -43,10 +45,6 @@ android {
versionCode = Versions.versionCode versionCode = Versions.versionCode
versionName = Versions.versionName versionName = Versions.versionName
vectorDrawables {
useSupportLibrary = true
}
// Keep abiFilter for the universalApk // Keep abiFilter for the universalApk
ndk { ndk {
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64") abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
@ -194,6 +192,26 @@ knit {
} }
} }
val ciBuildProperty = "ci-build"
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
val raw = project.property(ciBuildProperty) as? String
raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
} else {
false
}
kover {
// When running on the CI, run only debug test variants
if (isCiBuild) {
excludeTests {
// Disable instrumentation for debug test tasks
tasks(
"testDebugUnitTest",
)
}
}
}
dependencies { dependencies {
allLibrariesImpl() allLibrariesImpl()
allServicesImpl() allServicesImpl()

View file

@ -10,6 +10,9 @@
-keep class com.sun.jna.** { *; } -keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; } -keep class * implements com.sun.jna.** { *; }
# TagSoup, coming from the RTE library
-keep class org.ccil.cowan.tagsoup.** { *; }
# kotlinx.serialization # kotlinx.serialization
# Kotlin serialization looks up the generated serializer classes through a function on companion # Kotlin serialization looks up the generated serializer classes through a function on companion
@ -32,3 +35,8 @@
-dontwarn org.conscrypt.** -dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.** -dontwarn org.bouncycastle.**
-dontwarn org.openjsse.** -dontwarn org.openjsse.**
# Needed for Posthog
-keepclassmembers class android.view.JavaViewSpy {
static int windowAttachCount(android.view.View);
}

View file

@ -27,7 +27,6 @@ import io.element.android.x.initializer.CrashInitializer
import io.element.android.x.initializer.TracingInitializer import io.element.android.x.initializer.TracingInitializer
class ElementXApplication : Application(), DaggerComponentOwner { class ElementXApplication : Application(), DaggerComponentOwner {
override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this) override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this)
override fun onCreate() { override fun onCreate() {

View file

@ -51,7 +51,6 @@ import timber.log.Timber
private val loggerTag = LoggerTag("MainActivity") private val loggerTag = LoggerTag("MainActivity")
class MainActivity : NodeActivity() { class MainActivity : NodeActivity() {
private lateinit var mainNode: MainNode private lateinit var mainNode: MainNode
private lateinit var appBindings: AppBindings private lateinit var appBindings: AppBindings

View file

@ -48,7 +48,6 @@ class MainNode(
plugins = plugins, plugins = plugins,
), ),
DaggerComponentOwner { DaggerComponentOwner {
override val daggerComponent = (context as DaggerComponentOwner).daggerComponent override val daggerComponent = (context as DaggerComponentOwner).daggerComponent
override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node {
@ -68,5 +67,4 @@ class MainNode(
@Parcelize @Parcelize
object RootNavTarget : Parcelable object RootNavTarget : Parcelable
} }

View file

@ -27,8 +27,12 @@ import io.element.android.libraries.matrix.api.tracing.TracingService
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun snackbarDispatcher(): SnackbarDispatcher fun snackbarDispatcher(): SnackbarDispatcher
fun tracingService(): TracingService fun tracingService(): TracingService
fun bugReporter(): BugReporter fun bugReporter(): BugReporter
fun lockScreenService(): LockScreenService fun lockScreenService(): LockScreenService
fun preferencesStore(): PreferencesStore fun preferencesStore(): PreferencesStore
} }

View file

@ -28,9 +28,11 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@MergeComponent(AppScope::class) @MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings { interface AppComponent : NodeFactoriesBindings {
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(@ApplicationContext @BindsInstance context: Context): AppComponent fun create(
@ApplicationContext @BindsInstance
context: Context
): AppComponent
} }
} }

View file

@ -46,7 +46,6 @@ import java.io.File
@Module @Module
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
object AppModule { object AppModule {
@Provides @Provides
fun providesBaseDirectory(@ApplicationContext context: Context): File { fun providesBaseDirectory(@ApplicationContext context: Context): File {
return File(context.filesDir, "sessions") return File(context.filesDir, "sessions")
@ -82,14 +81,20 @@ object AppModule {
buildType = buildType, buildType = buildType,
applicationName = context.getString(R.string.app_name), applicationName = context.getString(R.string.app_name),
applicationId = BuildConfig.APPLICATION_ID, applicationId = BuildConfig.APPLICATION_ID,
lowPrivacyLoggingEnabled = false, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,
versionCode = BuildConfig.VERSION_CODE, versionCode = BuildConfig.VERSION_CODE,
gitRevision = "TODO", // BuildConfig.GIT_REVISION, // BuildConfig.GIT_REVISION,
gitRevisionDate = "TODO", // BuildConfig.GIT_REVISION_DATE, gitRevision = "TODO",
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME, // BuildConfig.GIT_REVISION_DATE,
flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION, gitRevisionDate = "TODO",
flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION, // BuildConfig.GIT_BRANCH_NAME,
gitBranchName = "TODO",
// BuildConfig.FLAVOR_DESCRIPTION,
flavorDescription = "TODO",
// BuildConfig.SHORT_FLAVOR_DESCRIPTION,
flavorShortDescription = "TODO",
) )
@Provides @Provides

View file

@ -26,7 +26,6 @@ import javax.inject.Inject
class DefaultRoomComponentFactory @Inject constructor( class DefaultRoomComponentFactory @Inject constructor(
private val roomComponentBuilder: RoomComponent.Builder private val roomComponentBuilder: RoomComponent.Builder
) : RoomComponentFactory { ) : RoomComponentFactory {
override fun create(room: MatrixRoom): Any { override fun create(room: MatrixRoom): Any {
return roomComponentBuilder.room(room).build() return roomComponentBuilder.room(room).build()
} }

View file

@ -26,7 +26,6 @@ import javax.inject.Inject
class DefaultSessionComponentFactory @Inject constructor( class DefaultSessionComponentFactory @Inject constructor(
private val sessionComponentBuilder: SessionComponent.Builder private val sessionComponentBuilder: SessionComponent.Builder
) : SessionComponentFactory { ) : SessionComponentFactory {
override fun create(client: MatrixClient): Any { override fun create(client: MatrixClient): Any {
return sessionComponentBuilder.client(client).build() return sessionComponentBuilder.client(client).build()
} }

View file

@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
@SingleIn(RoomScope::class) @SingleIn(RoomScope::class)
@MergeSubcomponent(RoomScope::class) @MergeSubcomponent(RoomScope::class)
interface RoomComponent : NodeFactoriesBindings { interface RoomComponent : NodeFactoriesBindings {
@Subcomponent.Builder @Subcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance

View file

@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
@MergeSubcomponent(SessionScope::class) @MergeSubcomponent(SessionScope::class)
interface SessionComponent : NodeFactoriesBindings { interface SessionComponent : NodeFactoriesBindings {
@Subcomponent.Builder @Subcomponent.Builder
interface Builder { interface Builder {
@BindsInstance @BindsInstance

View file

@ -21,7 +21,6 @@ import androidx.startup.Initializer
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
class CrashInitializer : Initializer<Unit> { class CrashInitializer : Initializer<Unit> {
override fun create(context: Context) { override fun create(context: Context) {
VectorUncaughtExceptionHandler(context).activate() VectorUncaughtExceptionHandler(context).activate()
} }

View file

@ -31,7 +31,6 @@ import io.element.android.x.di.AppBindings
import timber.log.Timber import timber.log.Timber
class TracingInitializer : Initializer<Unit> { class TracingInitializer : Initializer<Unit> {
override fun create(context: Context) { override fun create(context: Context) {
val appBindings = context.bindings<AppBindings>() val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService() val tracingService = appBindings.tracingService()

View file

@ -31,7 +31,6 @@ import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class IntentProviderImplTest { class IntentProviderImplTest {
@Test @Test
fun `test getViewRoomIntent with Session`() { fun `test getViewRoomIntent with Session`() {
val sut = createIntentProviderImpl() val sut = createIntentProviderImpl()

View file

@ -25,48 +25,27 @@ import kotlin.time.Duration.Companion.seconds
/** /**
* Configuration for the lock screen feature. * Configuration for the lock screen feature.
* @property isPinMandatory Whether the PIN is mandatory or not.
* @property pinBlacklist Some PINs are forbidden.
* @property pinSize The size of the PIN.
* @property maxPinCodeAttemptsBeforeLogout Number of attempts before the user is logged out.
* @property gracePeriod Time period before locking the app once backgrounded.
* @property isStrongBiometricsEnabled Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported.
* @property isWeakBiometricsEnabled Authentication with weak methods (most face/iris unlock implementations) is supported.
*/ */
data class LockScreenConfig( data class LockScreenConfig(
/**
* Whether the PIN is mandatory or not.
*/
val isPinMandatory: Boolean, val isPinMandatory: Boolean,
/**
* Some PINs are forbidden.
*/
val pinBlacklist: Set<String>, val pinBlacklist: Set<String>,
/**
* The size of the PIN.
*/
val pinSize: Int, val pinSize: Int,
/**
* Number of attempts before the user is logged out.
*/
val maxPinCodeAttemptsBeforeLogout: Int, val maxPinCodeAttemptsBeforeLogout: Int,
/**
* Time period before locking the app once backgrounded.
*/
val gracePeriod: Duration, val gracePeriod: Duration,
/**
* Authentication with strong methods (fingerprint, some face/iris unlock implementations) is supported.
*/
val isStrongBiometricsEnabled: Boolean, val isStrongBiometricsEnabled: Boolean,
/**
* Authentication with weak methods (most face/iris unlock implementations) is supported.
*/
val isWeakBiometricsEnabled: Boolean, val isWeakBiometricsEnabled: Boolean,
) )
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@Module @Module
object LockScreenConfigModule { object LockScreenConfigModule {
@Provides @Provides
fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig( fun providesLockScreenConfig(): LockScreenConfig = LockScreenConfig(
isPinMandatory = false, isPinMandatory = false,

View file

@ -17,6 +17,6 @@
package io.element.android.appconfig package io.element.android.appconfig
object MatrixConfiguration { object MatrixConfiguration {
const val matrixToPermalinkBaseUrl: String = "https://matrix.to/#/" const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
val clientPermalinkBaseUrl: String? = null val clientPermalinkBaseUrl: String? = null
} }

View file

@ -18,8 +18,8 @@ package io.element.android.appconfig
object NotificationConfig { object NotificationConfig {
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val supportMarkAsReadAction = false const val SUPPORT_MARK_AS_READ_ACTION = false
// TODO EAx Implement and set to true at some point // TODO EAx Implement and set to true at some point
const val supportQuickReplyAction = false const val SUPPORT_QUICK_REPLY_ACTION = false
} }

View file

@ -20,5 +20,5 @@ object PushConfig {
/** /**
* Note: pusher_app_id cannot exceed 64 chars. * Note: pusher_app_id cannot exceed 64 chars.
*/ */
const val pusher_app_id: String = "im.vector.app.android" const val PUSHER_APP_ID: String = "im.vector.app.android"
} }

View file

@ -17,8 +17,8 @@
package io.element.android.appconfig package io.element.android.appconfig
object RoomListConfig { object RoomListConfig {
const val showInviteMenuItem = false const val SHOW_INVITE_MENU_ITEM = false
const val showReportProblemMenuItem = false const val SHOW_REPORT_PROBLEM_MENU_ITEM = false
const val hasDropdownMenu = showInviteMenuItem || showReportProblemMenuItem const val HAS_DROP_DOWN_MENU = SHOW_INVITE_MENU_ITEM || SHOW_REPORT_PROBLEM_MENU_ITEM
} }

View file

@ -17,5 +17,5 @@
package io.element.android.appconfig package io.element.android.appconfig
object SecureBackupConfig { object SecureBackupConfig {
const val LearnMoreUrl: String = "https://element.io/help#encryption5" const val LEARN_MORE_URL: String = "https://element.io/help#encryption5"
} }

View file

@ -17,5 +17,5 @@
package io.element.android.appconfig package io.element.android.appconfig
object TimelineConfig { object TimelineConfig {
const val maxReadReceiptToDisplay = 3 const val MAX_READ_RECEIPT_TO_DISPLAY = 3
} }

View file

@ -40,4 +40,3 @@ fun <T : Any> BackStack<T>.removeLast(element: T) {
} ?: return } ?: return
accept(Remove(lastExpectedNavElement.key)) accept(Remove(lastExpectedNavElement.key))
} }

View file

@ -36,7 +36,6 @@ class LoggedInEventProcessor @Inject constructor(
roomMembershipObserver: RoomMembershipObserver, roomMembershipObserver: RoomMembershipObserver,
sessionVerificationService: SessionVerificationService, sessionVerificationService: SessionVerificationService,
) { ) {
private var observingJob: Job? = null private var observingJob: Job? = null
private val displayLeftRoomMessage = roomMembershipObserver.updates private val displayLeftRoomMessage = roomMembershipObserver.updates

View file

@ -111,7 +111,6 @@ class LoggedInFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenBugReport() fun onOpenBugReport()
} }
@ -138,7 +137,7 @@ class LoggedInFlowNode @AssistedInject constructor(
}, },
onStop = { onStop = {
coroutineScope.launch { coroutineScope.launch {
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method. // Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
syncService.stopSync() syncService.stopSync()
} }
}, },

View file

@ -81,7 +81,6 @@ class RootFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
override fun onBuilt() { override fun onBuilt() {
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap) matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
super.onBuilt() super.onBuilt()
@ -268,10 +267,9 @@ class RootFlowNode @AssistedInject constructor(
} }
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode { private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
//TODO handle multi-session // TODO handle multi-session
return waitForChildAttached { navTarget -> return waitForChildAttached { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId
} }
} }
} }

View file

@ -37,7 +37,6 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) : MatrixClientProvider { class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) : MatrixClientProvider {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>() private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>()
private val restoreMutex = Mutex() private val restoreMutex = Mutex()
@ -65,8 +64,9 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun restoreWithSavedState(state: SavedStateMap?) { fun restoreWithSavedState(state: SavedStateMap?) {
Timber.d("Restore state") Timber.d("Restore state")
if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also { if (state == null || sessionIdsToMatrixClient.isNotEmpty()) {
Timber.w("Restore with non-empty map") Timber.w("Restore with non-empty map")
return
} }
val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId> val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId>
Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}")

View file

@ -35,7 +35,6 @@ class LoggedInNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val loggedInState = loggedInPresenter.present() val loggedInState = loggedInPresenter.present()

View file

@ -35,7 +35,6 @@ class LoggedInPresenter @Inject constructor(
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val pushService: PushService, private val pushService: PushService,
) : Presenter<LoggedInState> { ) : Presenter<LoggedInState> {
@Composable @Composable
override fun present(): LoggedInState { override fun present(): LoggedInState {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {

View file

@ -59,15 +59,15 @@ fun SyncStateView(
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary) .background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp), .padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier modifier = Modifier
.progressSemantics() .progressSemantics()
.size(12.dp), .size(12.dp),
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
strokeWidth = 1.5.dp, strokeWidth = 1.5.dp,
) )

View file

@ -18,7 +18,6 @@ package io.element.android.appnav.room
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -43,7 +42,6 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun LoadingRoomNodeView( fun LoadingRoomNodeView(
state: LoadingRoomState, state: LoadingRoomState,

View file

@ -47,7 +47,6 @@ open class LoadingRoomStateProvider : PreviewParameterProvider<LoadingRoomState>
@SingleIn(SessionScope::class) @SingleIn(SessionScope::class)
class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) { class LoadingRoomStateFlowFactory @Inject constructor(private val matrixClient: MatrixClient) {
fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow<LoadingRoomState> = fun create(lifecycleScope: CoroutineScope, roomId: RoomId): StateFlow<LoadingRoomState> =
getRoomFlow(roomId) getRoomFlow(roomId)
.map { room -> .map { room ->

View file

@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.transition.JumpToEndTransitionHandler
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
@ -65,7 +66,6 @@ class RoomFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
data class Inputs( data class Inputs(
val roomId: RoomId, val roomId: RoomId,
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages, val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
@ -130,7 +130,8 @@ class RoomFlowNode @AssistedInject constructor(
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
BackstackView() BackstackView(
transitionHandler = JumpToEndTransitionHandler(),
)
} }
} }

View file

@ -71,7 +71,6 @@ class RoomLoadedFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
), DaggerComponentOwner { ), DaggerComponentOwner {
interface Callback : Plugin { interface Callback : Plugin {
fun onOpenRoom(roomId: RoomId) fun onOpenRoom(roomId: RoomId)
fun onForwardedToSingleRoom(roomId: RoomId) fun onForwardedToSingleRoom(roomId: RoomId)

View file

@ -41,7 +41,6 @@ class RootNavStateFlowFactory @Inject constructor(
private val matrixClientsHolder: MatrixClientsHolder, private val matrixClientsHolder: MatrixClientsHolder,
private val loginUserStory: LoginUserStory, private val loginUserStory: LoginUserStory,
) { ) {
private var currentCacheIndex = 0 private var currentCacheIndex = 0
fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> { fun create(savedStateMap: SavedStateMap?): Flow<RootNavState> {

View file

@ -30,7 +30,6 @@ class RootPresenter @Inject constructor(
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter, private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
private val appErrorStateService: AppErrorStateService, private val appErrorStateService: AppErrorStateService,
) : Presenter<RootState> { ) : Presenter<RootState> {
@Composable @Composable
override fun present(): RootState { override fun present(): RootState {
val rageshakeDetectionState = rageshakeDetectionPresenter.present() val rageshakeDetectionState = rageshakeDetectionPresenter.present()

View file

@ -41,7 +41,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
class RoomFlowNodeTest { class RoomFlowNodeTest {
@get:Rule @get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule() val instantTaskExecutorRule = InstantTaskExecutorRule()
@ -49,7 +48,6 @@ class RoomFlowNodeTest {
val mainDispatcherRule = MainDispatcherRule() val mainDispatcherRule = MainDispatcherRule()
private class FakeMessagesEntryPoint : MessagesEntryPoint { private class FakeMessagesEntryPoint : MessagesEntryPoint {
var nodeId: String? = null var nodeId: String? = null
var callback: MessagesEntryPoint.Callback? = null var callback: MessagesEntryPoint.Callback? = null
@ -68,12 +66,10 @@ class RoomFlowNodeTest {
} }
private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint { private class FakeRoomDetailsEntryPoint : RoomDetailsEntryPoint {
var nodeId: String? = null var nodeId: String? = null
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): RoomDetailsEntryPoint.NodeBuilder {
return object : RoomDetailsEntryPoint.NodeBuilder { return object : RoomDetailsEntryPoint.NodeBuilder {
override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder { override fun params(params: RoomDetailsEntryPoint.Params): RoomDetailsEntryPoint.NodeBuilder {
return this return this
} }

View file

@ -36,7 +36,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
class LoggedInPresenterTest { class LoggedInPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()

View file

@ -28,25 +28,24 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class LoadingRoomStateFlowFactoryTest { class LoadingRoomStateFlowFactoryTest {
@Test @Test
fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest { fun `flow should emit Loading and then Loaded when there is a room in cache`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID) val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)
val matrixClient = FakeMatrixClient(A_SESSION_ID).apply { val matrixClient = FakeMatrixClient(A_SESSION_ID).apply {
givenGetRoomResult(A_ROOM_ID, room) givenGetRoomResult(A_ROOM_ID, room)
} }
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory flowFactory
.create(this, A_ROOM_ID) .create(this, A_ROOM_ID)
.test { .test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
} }
} }
@Test @Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest { fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID) val room = FakeMatrixRoom(sessionId = A_SESSION_ID, roomId = A_ROOM_ID)
val roomListService = FakeRoomListService() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)

View file

@ -1,7 +1,5 @@
import com.google.devtools.ksp.gradle.KspTask import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript { buildscript {
dependencies { dependencies {
@ -28,6 +26,7 @@ buildscript {
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("io.element.android-root")
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
@ -41,7 +40,6 @@ plugins {
alias(libs.plugins.ktlint) alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph) alias(libs.plugins.dependencygraph)
alias(libs.plugins.sonarqube) alias(libs.plugins.sonarqube)
alias(libs.plugins.kover)
} }
tasks.register<Delete>("clean").configure { tasks.register<Delete>("clean").configure {
@ -62,7 +60,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml")) config.from(files("$rootDir/tools/detekt/detekt.yml"))
} }
dependencies { dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.3.8") detektPlugins("io.nlopez.compose.rules:detekt:0.3.9")
} }
// KtLint // KtLint
@ -74,7 +72,7 @@ allprojects {
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> { configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
// See https://github.com/pinterest/ktlint/releases/ // See https://github.com/pinterest/ktlint/releases/
// TODO Regularly check for new version here ^ // TODO Regularly check for new version here ^
version.set("0.48.2") version.set("1.1.1")
android.set(true) android.set(true)
ignoreFailures.set(false) ignoreFailures.set(false)
enableExperimentalRules.set(true) enableExperimentalRules.set(true)
@ -100,11 +98,16 @@ allprojects {
// You can override by passing `-PallWarningsAsErrors=true` in the command line // You can override by passing `-PallWarningsAsErrors=true` in the command line
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file // Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true" kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
}
// Detect unused dependencies kotlinOptions {
apply { /*
plugin("com.autonomousapps.dependency-analysis") // Uncomment to suppress Compose Kotlin compiler compatibility warning
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=true"
)
*/
}
} }
} }
@ -158,180 +161,6 @@ allprojects {
} }
} }
allprojects {
apply(plugin = "kover")
}
// https://kotlin.github.io/kotlinx-kover/
// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover
// Run `./gradlew koverMergedReport` to also get XML report
koverMerged {
enable()
filters {
classes {
excludes.addAll(
listOf(
// Exclude generated classes.
"*_ModuleKt",
"anvil.hint.binding.io.element.*",
"anvil.hint.merge.*",
"anvil.hint.multibinding.io.element.*",
"anvil.module.*",
"com.airbnb.android.showkase*",
"io.element.android.libraries.designsystem.showkase.*",
"io.element.android.x.di.DaggerAppComponent*",
"*_Factory",
"*_Factory_Impl",
"*_Factory$*",
"*_Module",
"*_Module$*",
"*Module_Provides*",
"Dagger*Component*",
"*ComposableSingletons$*",
"*_AssistedFactory_Impl*",
"*BuildConfig",
// Generated by Showkase
"*Ioelementandroid*PreviewKt$*",
"*Ioelementandroid*PreviewKt",
// Other
// We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
"*Node",
"*Node$*",
// Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix SDK api, so it is not really relevant to unit test it: there is no logic to test.
"io.element.android.libraries.matrix.impl.*",
"*Presenter\$present\$*",
// Forked from compose
"io.element.android.libraries.designsystem.theme.components.bottomsheet.*",
)
)
}
annotations {
excludes.addAll(
listOf(
"*Preview",
)
)
}
projects {
excludes.addAll(
listOf(
":anvilannotations",
":anvilcodegen",
":samples:minimal",
":tests:testutils",
)
)
}
}
// Run ./gradlew koverMergedVerify to check the rules.
verify {
// Does not seems to work, so also run the task manually on the workflow.
onCheck.set(true)
// General rule: minimum code coverage.
rule {
name = "Global minimum code coverage."
target = kotlinx.kover.api.VerificationTarget.ALL
bound {
minValue = 65
// Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
// For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
// minValue to 25 and maxValue to 35.
maxValue = 75
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of Presenters is sufficient.
rule {
name = "Check code coverage of presenters"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "*Presenter"
excludes += "*Fake*Presenter"
excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
// Some options can't be tested at the moment
excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
excludes += "*Presenter\$present\$*"
}
bound {
minValue = 85
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of States is sufficient.
rule {
name = "Check code coverage of states"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "^*State$"
excludes += "io.element.android.appnav.root.RootNavState*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
excludes += "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*"
excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
excludes += "io.element.android.features.location.impl.map.MapState*"
excludes += "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*"
excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
excludes += "io.element.android.features.ftue.api.state.*"
excludes += "io.element.android.features.ftue.impl.welcome.state.*"
}
bound {
minValue = 90
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
// Rule to ensure that coverage of Views is sufficient (deactivated for now).
rule {
name = "Check code coverage of views"
target = kotlinx.kover.api.VerificationTarget.CLASS
overrideClassFilter {
includes += "*ViewKt"
}
bound {
// TODO Update this value, for now there are too many missing tests.
minValue = 0
counter = kotlinx.kover.api.CounterType.INSTRUCTION
valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
}
}
}
}
// When running on the CI, run only debug test variants
val ciBuildProperty = "ci-build"
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
val raw = project.property(ciBuildProperty) as? String
raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
} else {
false
}
if (isCiBuild) {
allprojects {
afterEvaluate {
tasks.withType<Test>().configureEach {
extensions.configure<KoverTaskExtension> {
val enabled = name.contains("debug", ignoreCase = true)
isDisabled.set(!enabled)
}
}
}
}
}
// Register quality check tasks. // Register quality check tasks.
tasks.register("runQualityChecks") { tasks.register("runQualityChecks") {
project.subprojects { project.subprojects {
@ -361,7 +190,7 @@ subprojects {
// Workaround for https://github.com/airbnb/Showkase/issues/335 // Workaround for https://github.com/airbnb/Showkase/issues/335
subprojects { subprojects {
tasks.withType<KspTask>() { tasks.withType<KspTask> {
doLast { doLast {
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file -> fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
ReplaceRegExp().apply { ReplaceRegExp().apply {
@ -383,13 +212,15 @@ subprojects {
if (project.findProperty("composeCompilerReports") == "true") { if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
) )
} }
if (project.findProperty("composeCompilerMetrics") == "true") { if (project.findProperty("composeCompilerMetrics") == "true") {
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-P", "-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler" "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
"${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler"
) )
} }
} }

View file

@ -344,26 +344,26 @@ implementation of our interfaces. Mocking can be used to mock Android framework
[kover](https://github.com/Kotlin/kotlinx-kover) is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does [kover](https://github.com/Kotlin/kotlinx-kover) is used to compute code coverage. Only have unit tests can produce code coverage result. Running Maestro does
not participate to the code coverage results. not participate to the code coverage results.
Kover configuration is defined in the main [build.gradle.kts](../build.gradle.kts) file. Kover configuration is defined in the app [build.gradle.kts](../app/build.gradle.kts) file.
To compute the code coverage, run: To compute the code coverage, run:
```bash ```bash
./gradlew koverMergedReport ./gradlew :app:koverHtmlReport
``` ```
and open the Html report: [../build/reports/kover/merged/html/index.html](../build/reports/kover/merged/html/index.html) and open the Html report: [../app/build/reports/kover/html/index.html](../app/build/reports/kover/html/index.html)
To ensure that the code coverage threshold are OK, you can run To ensure that the code coverage threshold are OK, you can run
```bash ```bash
./gradlew koverMergedVerify ./gradlew :app:koverVerify
``` ```
Note that the CI performs this check on every pull requests. Note that the CI performs this check on every pull requests.
Also, if the rule `Global minimum code coverage.` is in error because code coverage is `> maxValue`, `minValue` and `maxValue` can be updated for this rule in Also, if the rule `Global minimum code coverage.` is in error because code coverage is `> maxValue`, `minValue` and `maxValue` can be updated for this rule in
the file [build.gradle.kts](../build.gradle.kts) (you will see further instructions there). the file [build.gradle.kts](../app/build.gradle.kts) (you will see further instructions there).
### Other points ### Other points

View file

@ -8,7 +8,7 @@
* [Stop Synapse](#stop-synapse) * [Stop Synapse](#stop-synapse)
* [Troubleshoot](#troubleshoot) * [Troubleshoot](#troubleshoot)
* [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver) * [Android Emulator does cannot reach the homeserver](#android-emulator-does-cannot-reach-the-homeserver)
* [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost:8080") * [Tests partially run but some fail with "Unable to contact localhost:8080"](#tests-partially-run-but-some-fail-with-"unable-to-contact-localhost8080")
* [virtualenv command fails](#virtualenv-command-fails) * [virtualenv command fails](#virtualenv-command-fails)
<!--- END --> <!--- END -->

View file

@ -0,0 +1,2 @@
Main changes in this version: Mainly bug fixes.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -25,8 +25,8 @@ import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.R import io.element.android.features.analytics.api.R
import io.element.android.libraries.designsystem.components.LINK_TAG import io.element.android.libraries.designsystem.components.LINK_TAG
import io.element.android.libraries.designsystem.components.list.ListItemContent import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart 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.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSupportingText import io.element.android.libraries.designsystem.theme.components.ListSupportingText

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_share_data">"Compartir datos analíticos"</string>
<string name="screen_analytics_settings_help_us_improve">"Compartir datos de uso anónimos para ayudarnos a identificar problemas."</string>
<string name="screen_analytics_settings_read_terms">"Puedes leer todos nuestros términos %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"aquí"</string>
</resources>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_share_data">"Condividi dati statistici"</string>
<string name="screen_analytics_settings_help_us_improve">"Condividi dati di utilizzo anonimi per aiutarci a identificare problemi."</string>
<string name="screen_analytics_settings_read_terms">"Puoi leggere tutti i nostri termini %1$s."</string>
<string name="screen_analytics_settings_read_terms_content_link">"qui"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_share_data">"分享分析數據"</string> <string name="screen_analytics_settings_share_data">"提供分析數據"</string>
<string name="screen_analytics_settings_help_us_improve">"分享匿名的使用數據以協助我們釐清問題。"</string> <string name="screen_analytics_settings_help_us_improve">"提供匿名的使用數據以協助我們釐清問題。"</string>
<string name="screen_analytics_settings_read_terms">"您可以到%1$s閱讀我們的條款。"</string> <string name="screen_analytics_settings_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
<string name="screen_analytics_settings_read_terms_content_link">"這裡"</string> <string name="screen_analytics_settings_read_terms_content_link">"這裡"</string>
</resources> </resources>

View file

@ -37,7 +37,6 @@ class AnalyticsOptInNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: AnalyticsOptInPresenter, private val presenter: AnalyticsOptInPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
private fun onClickTerms(activity: Activity, darkTheme: Boolean) { private fun onClickTerms(activity: Activity, darkTheme: Boolean) {
activity.openUrlInChromeCustomTab(null, darkTheme, AnalyticsConfig.POLICY_LINK) activity.openUrlInChromeCustomTab(null, darkTheme, AnalyticsConfig.POLICY_LINK)
} }

View file

@ -30,7 +30,6 @@ class AnalyticsOptInPresenter @Inject constructor(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : Presenter<AnalyticsOptInState> { ) : Presenter<AnalyticsOptInState> {
@Composable @Composable
override fun present(): AnalyticsOptInState { override fun present(): AnalyticsOptInState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()

View file

@ -19,8 +19,7 @@ package io.element.android.features.analytics.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import javax.inject.Inject import javax.inject.Inject
open class AnalyticsOptInStateProvider @Inject constructor( open class AnalyticsOptInStateProvider @Inject constructor() : PreviewParameterProvider<AnalyticsOptInState> {
) : PreviewParameterProvider<AnalyticsOptInState> {
override val values: Sequence<AnalyticsOptInState> override val values: Sequence<AnalyticsOptInState>
get() = sequenceOf( get() = sequenceOf(
aAnalyticsOptInState(), aAnalyticsOptInState(),

View file

@ -36,7 +36,6 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : AnalyticsPreferencesPresenter { ) : AnalyticsPreferencesPresenter {
@Composable @Composable
override fun present(): AnalyticsPreferencesState { override fun present(): AnalyticsPreferencesState {
val localCoroutineScope = rememberCoroutineScope() val localCoroutineScope = rememberCoroutineScope()

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"No registraremos o perfilaremos ningún dato personal"</string>
<string name="screen_analytics_prompt_help_us_improve">"Compartir datos de uso anónimos para ayudarnos a identificar problemas."</string>
<string name="screen_analytics_prompt_read_terms">"Puedes leer todos nuestros términos %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"aquí"</string>
<string name="screen_analytics_prompt_settings">"Puedes desactivarlo en cualquier momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"No compartiremos tus datos con terceros"</string>
<string name="screen_analytics_prompt_title">"Ayuda a mejorar %1$s"</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"Non registreremo né profileremo alcun dato personale"</string>
<string name="screen_analytics_prompt_help_us_improve">"Condividi dati di utilizzo anonimi per aiutarci a identificare problemi."</string>
<string name="screen_analytics_prompt_read_terms">"Puoi leggere tutti i nostri termini %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"qui"</string>
<string name="screen_analytics_prompt_settings">"Puoi disattivarlo in qualsiasi momento"</string>
<string name="screen_analytics_prompt_third_party_sharing">"Non condivideremo i tuoi dati con terze parti"</string>
<string name="screen_analytics_prompt_title">"Aiutaci a migliorare %1$s"</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string> <string name="screen_analytics_prompt_data_usage">"我們不會紀錄或剖繪您的個人資料"</string>
<string name="screen_analytics_prompt_help_us_improve">"分享匿名的使用數據以協助我們釐清問題。"</string> <string name="screen_analytics_prompt_help_us_improve">"提供匿名的使用數據以協助我們釐清問題。"</string>
<string name="screen_analytics_prompt_read_terms">"您可以到%1$s閱讀我們的條款。"</string> <string name="screen_analytics_prompt_read_terms">"您可以到%1$s閱讀我們的條款。"</string>
<string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string> <string name="screen_analytics_prompt_read_terms_content_link">"這裡"</string>
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string> <string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>

View file

@ -30,7 +30,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
class AnalyticsOptInPresenterTest { class AnalyticsOptInPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -70,4 +69,3 @@ class AnalyticsOptInPresenterTest {
} }
} }
} }

View file

@ -29,7 +29,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
class AnalyticsPreferencesPresenterTest { class AnalyticsPreferencesPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -82,4 +81,3 @@ class AnalyticsPreferencesPresenterTest {
} }
} }
} }

View file

@ -30,7 +30,6 @@ import io.element.android.features.call.ui.ElementCallActivity
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
class CallForegroundService : Service() { class CallForegroundService : Service() {
companion object { companion object {
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, CallForegroundService::class.java) val intent = Intent(context, CallForegroundService::class.java)

View file

@ -28,11 +28,11 @@ data class WidgetMessage(
@SerialName("action") val action: Action, @SerialName("action") val action: Action,
@SerialName("data") val data: JsonElement? = null, @SerialName("data") val data: JsonElement? = null,
) { ) {
@Serializable @Serializable
enum class Direction { enum class Direction {
@SerialName("fromWidget") @SerialName("fromWidget")
FromWidget, FromWidget,
@SerialName("toWidget") @SerialName("toWidget")
ToWidget ToWidget
} }
@ -41,6 +41,7 @@ data class WidgetMessage(
enum class Action { enum class Action {
@SerialName("im.vector.hangup") @SerialName("im.vector.hangup")
HangUp, HangUp,
@SerialName("send_event") @SerialName("send_event")
SendEvent, SendEvent,
} }

View file

@ -34,7 +34,7 @@ import io.element.android.features.call.data.WidgetMessage
import io.element.android.features.call.utils.CallWidgetProvider import io.element.android.features.call.utils.CallWidgetProvider
import io.element.android.features.call.utils.WidgetMessageInterceptor import io.element.android.features.call.utils.WidgetMessageInterceptor
import io.element.android.features.call.utils.WidgetMessageSerializer import io.element.android.features.call.utils.WidgetMessageSerializer
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@ -63,7 +63,6 @@ class CallScreenPresenter @AssistedInject constructor(
private val matrixClientsProvider: MatrixClientProvider, private val matrixClientsProvider: MatrixClientProvider,
private val appCoroutineScope: CoroutineScope, private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> { ) : Presenter<CallScreenState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter
@ -75,7 +74,7 @@ class CallScreenPresenter @AssistedInject constructor(
@Composable @Composable
override fun present(): CallScreenState { override fun present(): CallScreenState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val urlState = remember { mutableStateOf<Async<String>>(Async.Uninitialized) } val urlState = remember { mutableStateOf<AsyncData<String>>(AsyncData.Uninitialized) }
val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) } val callWidgetDriver = remember { mutableStateOf<MatrixWidgetDriver?>(null) }
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) } val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) } var isJoinedCall by rememberSaveable { mutableStateOf(false) }
@ -154,7 +153,7 @@ class CallScreenPresenter @AssistedInject constructor(
private fun CoroutineScope.loadUrl( private fun CoroutineScope.loadUrl(
inputs: CallType, inputs: CallType,
urlState: MutableState<Async<String>>, urlState: MutableState<AsyncData<String>>,
callWidgetDriver: MutableState<MatrixWidgetDriver?>, callWidgetDriver: MutableState<MatrixWidgetDriver?>,
) = launch { ) = launch {
urlState.runCatchingUpdatingState { urlState.runCatchingUpdatingState {
@ -224,6 +223,4 @@ class CallScreenPresenter @AssistedInject constructor(
navigator.close() navigator.close()
widgetDriver?.close() widgetDriver?.close()
} }
} }

View file

@ -16,10 +16,10 @@
package io.element.android.features.call.ui package io.element.android.features.call.ui
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
data class CallScreenState( data class CallScreenState(
val urlState: Async<String>, val urlState: AsyncData<String>,
val userAgent: String, val userAgent: String,
val isInWidgetMode: Boolean, val isInWidgetMode: Boolean,
val eventSink: (CallScreenEvents) -> Unit, val eventSink: (CallScreenEvents) -> Unit,

View file

@ -36,7 +36,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.call.R import io.element.android.features.call.R
import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor import io.element.android.features.call.utils.WebViewWidgetMessageInterceptor
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -96,7 +96,7 @@ internal fun CallScreenView(
@Composable @Composable
private fun CallWebView( private fun CallWebView(
url: Async<String>, url: AsyncData<String>,
userAgent: String, userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit, onPermissionsRequested: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit, onWebViewCreated: (WebView) -> Unit,
@ -116,7 +116,7 @@ private fun CallWebView(
} }
}, },
update = { webView -> update = { webView ->
if (url is Async.Success && webView.url != url.data) { if (url is AsyncData.Success && webView.url != url.data) {
webView.loadUrl(url.data) webView.loadUrl(url.data)
} }
}, },
@ -161,7 +161,7 @@ internal fun CallScreenViewPreview() {
ElementPreview { ElementPreview {
CallScreenView( CallScreenView(
state = CallScreenState( state = CallScreenState(
urlState = Async.Success("https://call.element.io/some-actual-call?with=parameters"), urlState = AsyncData.Success("https://call.element.io/some-actual-call?with=parameters"),
isInWidgetMode = false, isInWidgetMode = false,
userAgent = "", userAgent = "",
eventSink = {}, eventSink = {},

View file

@ -20,7 +20,6 @@ import android.net.Uri
import javax.inject.Inject import javax.inject.Inject
class CallIntentDataParser @Inject constructor() { class CallIntentDataParser @Inject constructor() {
private val validHttpSchemes = sequenceOf("https") private val validHttpSchemes = sequenceOf("https")
fun parse(data: String?): String? { fun parse(data: String?): String? {

View file

@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
class WebViewWidgetMessageInterceptor( class WebViewWidgetMessageInterceptor(
private val webView: WebView, private val webView: WebView,
) : WidgetMessageInterceptor { ) : WidgetMessageInterceptor {
companion object { companion object {
// We call both the WebMessageListener and the JavascriptInterface objects in JS with this // We call both the WebMessageListener and the JavascriptInterface objects in JS with this
// 'listenerName' so they can both receive the data from the WebView when // 'listenerName' so they can both receive the data from the WebView when
@ -56,7 +55,7 @@ class WebViewWidgetMessageInterceptor(
|| !message.data.response && message.data.api == "fromWidget") { || !message.data.response && message.data.api == "fromWidget") {
let json = JSON.stringify(event.data) let json = JSON.stringify(event.data)
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } } ${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } }
${LISTENER_NAME}.postMessage(json); $LISTENER_NAME.postMessage(json);
} else { } else {
${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } } ${"console.log('message received (ignored): ' + JSON.stringify(event.data));".takeIf { BuildConfig.DEBUG } }
} }

View file

@ -20,7 +20,6 @@ import io.element.android.features.call.data.WidgetMessage
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
object WidgetMessageSerializer { object WidgetMessageSerializer {
private val coder = Json { ignoreUnknownKeys = true } private val coder = Json { ignoreUnknownKeys = true }
fun deserialize(message: String): Result<WidgetMessage> { fun deserialize(message: String): Result<WidgetMessage> {

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Llamada en curso"</string>
<string name="call_foreground_service_message_android">"Pulsa para regresar a la llamada"</string>
<string name="call_foreground_service_title_android">"☎️ Llamada en curso"</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Folyamatban lévő hívás"</string> <string name="call_foreground_service_channel_title_android">"Folyamatban lévő hívás"</string>
<string name="call_foreground_service_message_android">"Koppintson a híváshoz való visszatéréshez"</string> <string name="call_foreground_service_message_android">"Koppints a híváshoz való visszatéréshez"</string>
<string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</string> <string name="call_foreground_service_title_android">"☎️ Hívás folyamatban"</string>
</resources> </resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Chiamata in corso"</string>
<string name="call_foreground_service_message_android">"Tocca per tornare alla chiamata"</string>
<string name="call_foreground_service_title_android">"☎️ Chiamata in corso"</string>
</resources>

View file

@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="call_foreground_service_channel_title_android">"Текущий вызов"</string> <string name="call_foreground_service_channel_title_android">"Текущий вызов"</string>
<string name="call_foreground_service_message_android">"Коснитесь, чтобы вернуться к вызову"</string> <string name="call_foreground_service_message_android">"Коснитесь, чтобы вернуться к вызову"</string>
<string name="call_foreground_service_title_android">"Идёт вызов"</string> <string name="call_foreground_service_title_android">"☎️ Идёт вызов"</string>
</resources> </resources>

View file

@ -23,7 +23,6 @@ import io.element.android.features.call.ui.mapWebkitPermissions
import org.junit.Test import org.junit.Test
class MapWebkitPermissionsTest { class MapWebkitPermissionsTest {
@Test @Test
fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() { fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() {
val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) val permission = mapWebkitPermissions(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE))

View file

@ -23,7 +23,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.CallType import io.element.android.features.call.CallType
import io.element.android.features.call.utils.FakeCallWidgetProvider import io.element.android.features.call.utils.FakeCallWidgetProvider
import io.element.android.features.call.utils.FakeWidgetMessageInterceptor import io.element.android.features.call.utils.FakeWidgetMessageInterceptor
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
@ -48,7 +48,6 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
class CallScreenPresenterTest { class CallScreenPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -62,7 +61,7 @@ class CallScreenPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isEqualTo(Async.Success("https://call.element.io")) assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.isInWidgetMode).isFalse() assertThat(initialState.isInWidgetMode).isFalse()
} }
} }
@ -83,7 +82,7 @@ class CallScreenPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java) assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isInWidgetMode).isTrue() assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue() assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1) assertThat(widgetDriver.runCalledCount).isEqualTo(1)
@ -191,7 +190,7 @@ class CallScreenPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
consumeItemsUntilTimeout() consumeItemsUntilTimeout()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running) assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running)

View file

@ -17,10 +17,10 @@
package io.element.android.features.call.ui package io.element.android.features.call.ui
class FakeCallScreenNavigator : CallScreenNavigator { class FakeCallScreenNavigator : CallScreenNavigator {
var closeCalled = false var closeCalled = false
private set private set
override fun close() { override fun close() {
closeCalled = true closeCalled = true
}
} }
}

View file

@ -24,7 +24,6 @@ import java.net.URLEncoder
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class CallIntentDataParserTest { class CallIntentDataParserTest {
private val callIntentDataParser = CallIntentDataParser() private val callIntentDataParser = CallIntentDataParser()
@Test @Test
@ -117,7 +116,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param appPrompt gets url extracted`() { fun `Element Call url with url extra param appPrompt gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}&appPrompt=true", url = "$VALID_CALL_URL_WITH_PARAM&appPrompt=true",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
) )
} }
@ -125,7 +124,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() { fun `Element Call url with url extra param in fragment appPrompt gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true", url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&confineToRoom=true"
) )
} }
@ -133,7 +132,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() { fun `Element Call url with url extra param in fragment appPrompt and other gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?appPrompt=true&otherParam=maybe", url = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=true&otherParam=maybe",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?appPrompt=false&otherParam=maybe&confineToRoom=true"
) )
} }
@ -141,7 +140,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param confineToRoom gets url extracted`() { fun `Element Call url with url extra param confineToRoom gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}&confineToRoom=false", url = "$VALID_CALL_URL_WITH_PARAM&confineToRoom=false",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
) )
} }
@ -149,7 +148,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() { fun `Element Call url with url extra param in fragment confineToRoom gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false", url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&appPrompt=false"
) )
} }
@ -157,7 +156,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() { fun `Element Call url with url extra param in fragment confineToRoom and more gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?confineToRoom=false&otherParam=maybe", url = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=false&otherParam=maybe",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?confineToRoom=true&otherParam=maybe&appPrompt=false"
) )
} }
@ -165,7 +164,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url fragment gets url extracted`() { fun `Element Call url with url fragment gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#fragment", url = "$VALID_CALL_URL_WITH_PARAM#fragment",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?$EXTRA_PARAMS"
) )
} }
@ -173,7 +172,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url fragment with params gets url extracted`() { fun `Element Call url with url fragment with params gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#fragment?otherParam=maybe", url = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#fragment?otherParam=maybe&$EXTRA_PARAMS"
) )
} }
@ -181,7 +180,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with url fragment with other params gets url extracted`() { fun `Element Call url with url fragment with other params gets url extracted`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?otherParam=maybe", url = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?otherParam=maybe&$EXTRA_PARAMS"
) )
} }
@ -189,7 +188,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with empty fragment`() { fun `Element Call url with empty fragment`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#", url = "$VALID_CALL_URL_WITH_PARAM#",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
) )
} }
@ -197,7 +196,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `Element Call url with empty fragment query`() { fun `Element Call url with empty fragment query`() {
doTest( doTest(
url = "${VALID_CALL_URL_WITH_PARAM}#?", url = "$VALID_CALL_URL_WITH_PARAM#?",
expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS" expectedResult = "$VALID_CALL_URL_WITH_PARAM#?$EXTRA_PARAMS"
) )
} }

View file

@ -32,7 +32,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
class DefaultCallWidgetProviderTest { class DefaultCallWidgetProviderTest {
@Test @Test
fun `getWidget - fails if the session does not exist`() = runTest { fun `getWidget - fails if the session does not exist`() = runTest {
val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) }) val provider = createProvider(matrixClientProvider = FakeMatrixClientProvider { Result.failure(Exception("Session not found")) })

View file

@ -24,19 +24,18 @@ import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
class FakeCallWidgetProvider( class FakeCallWidgetProvider(
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(), private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
private val url: String = "https://call.element.io", private val url: String = "https://call.element.io",
) : CallWidgetProvider { ) : CallWidgetProvider {
var getWidgetCalled = false
private set
var getWidgetCalled = false override suspend fun getWidget(
private set sessionId: SessionId,
roomId: RoomId,
override suspend fun getWidget( clientId: String,
sessionId: SessionId, languageTag: String?,
roomId: RoomId, theme: String?
clientId: String, ): Result<Pair<MatrixWidgetDriver, String>> {
languageTag: String?, getWidgetCalled = true
theme: String? return Result.success(widgetDriver to url)
): Result<Pair<MatrixWidgetDriver, String>> {
getWidgetCalled = true
return Result.success(widgetDriver to url)
}
} }
}

View file

@ -19,15 +19,15 @@ package io.element.android.features.call.utils
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
class FakeWidgetMessageInterceptor : WidgetMessageInterceptor { class FakeWidgetMessageInterceptor : WidgetMessageInterceptor {
val sentMessages = mutableListOf<String>() val sentMessages = mutableListOf<String>()
override val interceptedMessages = MutableSharedFlow<String>(extraBufferCapacity = 1) override val interceptedMessages = MutableSharedFlow<String>(extraBufferCapacity = 1)
override fun sendMessage(message: String) { override fun sendMessage(message: String) {
sentMessages += message sentMessages += message
}
fun givenInterceptedMessage(message: String) {
interceptedMessages.tryEmit(message)
}
} }
fun givenInterceptedMessage(message: String) {
interceptedMessages.tryEmit(message)
}
}

View file

@ -23,7 +23,6 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
interface CreateRoomEntryPoint : FeatureEntryPoint { interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder { interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder fun callback(callback: Callback): NodeBuilder

View file

@ -17,7 +17,7 @@
package io.element.android.features.createroom.api package io.element.android.features.createroom.api
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -27,5 +27,5 @@ interface StartDMAction {
* @param userId The user to start a DM with. * @param userId The user to start a DM with.
* @param actionState The state to update with the result of the action. * @param actionState The state to update with the result of the action.
*/ */
suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>)
} }

View file

@ -52,7 +52,6 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
private val component by lazy { private val component by lazy {
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build() parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
} }

View file

@ -33,7 +33,6 @@ import javax.inject.Inject
class CreateRoomDataStore @Inject constructor( class CreateRoomDataStore @Inject constructor(
val selectedUserListDataStore: UserListDataStore, val selectedUserListDataStore: UserListDataStore,
) { ) {
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig()) private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null private var cachedAvatarUri: Uri? = null
set(value) { set(value) {

View file

@ -50,7 +50,6 @@ class CreateRoomFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins plugins = plugins
) { ) {
sealed interface NavTarget : Parcelable { sealed interface NavTarget : Parcelable {
@Parcelize @Parcelize
data object Root : NavTarget data object Root : NavTarget

View file

@ -27,13 +27,10 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint { class DefaultCreateRoomEntryPoint @Inject constructor() : CreateRoomEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder { override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): CreateRoomEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>() val plugins = ArrayList<Plugin>()
return object : CreateRoomEntryPoint.NodeBuilder { return object : CreateRoomEntryPoint.NodeBuilder {
override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder { override fun callback(callback: CreateRoomEntryPoint.Callback): CreateRoomEntryPoint.NodeBuilder {
plugins += callback plugins += callback
return this return this

View file

@ -20,7 +20,7 @@ import androidx.compose.runtime.MutableState
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.api.StartDMAction import io.element.android.features.createroom.api.StartDMAction
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -35,18 +35,17 @@ class DefaultStartDMAction @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : StartDMAction { ) : StartDMAction {
override suspend fun execute(userId: UserId, actionState: MutableState<AsyncAction<RoomId>>) {
override suspend fun execute(userId: UserId, actionState: MutableState<Async<RoomId>>) { actionState.value = AsyncAction.Loading
actionState.value = Async.Loading()
when (val result = matrixClient.startDM(userId)) { when (val result = matrixClient.startDM(userId)) {
is StartDMResult.Success -> { is StartDMResult.Success -> {
if (result.isNew) { if (result.isNew) {
analyticsService.capture(CreatedRoom(isDM = true)) analyticsService.capture(CreatedRoom(isDM = true))
} }
actionState.value = Async.Success(result.roomId) actionState.value = AsyncAction.Success(result.roomId)
} }
is StartDMResult.Failure -> { is StartDMResult.Failure -> {
actionState.value = Async.Failure(result.throwable) actionState.value = AsyncAction.Failure(result.throwable)
} }
} }
} }

View file

@ -33,7 +33,6 @@ class AddPeopleNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: AddPeoplePresenter, private val presenter: AddPeoplePresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin { interface Callback : Plugin {
fun onContinue() fun onContinue()
} }

View file

@ -31,7 +31,6 @@ class AddPeoplePresenter @Inject constructor(
userRepository: UserRepository, userRepository: UserRepository,
dataStore: CreateRoomDataStore, dataStore: CreateRoomDataStore,
) : Presenter<UserListState> { ) : Presenter<UserListState> {
private val userListPresenter = userListPresenterFactory.create( private val userListPresenter = userListPresenterFactory.create(
UserListPresenterArgs( UserListPresenterArgs(
selectionMode = SelectionMode.Multiple, selectionMode = SelectionMode.Multiple,
@ -45,4 +44,3 @@ class AddPeoplePresenter @Inject constructor(
return userListPresenter.present() return userListPresenter.present()
} }
} }

View file

@ -37,11 +37,13 @@ open class AddPeopleUserListStateProvider : PreviewParameterProvider<UserListSta
selectionMode = SelectionMode.Multiple, selectionMode = SelectionMode.Multiple,
), ),
aUserListState().copy( aUserListState().copy(
searchResults = SearchBarResultState.Results(aMatrixUserList() searchResults = SearchBarResultState.Results(
.mapIndexed { index, matrixUser -> aMatrixUserList()
UserSearchResult(matrixUser, index % 2 == 0) .mapIndexed { index, matrixUser ->
} UserSearchResult(matrixUser, index % 2 == 0)
.toImmutableList()), }
.toImmutableList()
),
selectedUsers = aListOfSelectedUsers(), selectedUsers = aListOfSelectedUsers(),
isSearchActive = true, isSearchActive = true,
selectionMode = SelectionMode.Multiple, selectionMode = SelectionMode.Multiple,

View file

@ -17,7 +17,6 @@
package io.element.android.features.createroom.impl.addpeople package io.element.android.features.createroom.impl.addpeople
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -27,6 +26,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.R import io.element.android.features.createroom.impl.R
import io.element.android.features.createroom.impl.components.UserListView import io.element.android.features.createroom.impl.components.UserListView
import io.element.android.features.createroom.impl.userlist.UserListEvents import io.element.android.features.createroom.impl.userlist.UserListEvents
@ -39,10 +39,8 @@ 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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun AddPeopleView( fun AddPeopleView(
state: UserListState, state: UserListState,

View file

@ -29,14 +29,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem
import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.compound.theme.ElementTheme
@Composable @Composable
fun RoomPrivacyOption( fun RoomPrivacyOption(
@ -85,7 +85,8 @@ fun RoomPrivacyOption(
.align(Alignment.CenterVertically) .align(Alignment.CenterVertically)
.size(48.dp), .size(48.dp),
selected = isSelected, selected = isSelected,
onClick = null // null recommended for accessibility with screenreaders // null recommended for accessibility with screenreaders
onClick = null
) )
} }
} }

View file

@ -57,10 +57,7 @@ fun SearchMultipleUsersResultItem(
@Preview @Preview
@Composable @Composable
internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() } internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview {
@Composable
private fun ContentToPreview() {
Column { Column {
SearchMultipleUsersResultItem( SearchMultipleUsersResultItem(
searchResult = UserSearchResult( searchResult = UserSearchResult(

Some files were not shown because too many files have changed in this diff Show more