Merge branch 'release/0.4.1' into main

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

View File

@@ -14,10 +14,19 @@ ij_smart_tabs = false
ij_visual_guides = none
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_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]
ij_java_align_consecutive_assignments = false

View File

@@ -18,12 +18,6 @@
],
"groupName" : "kotlin"
},
{
"matchPackageNames" : [
"org.jetbrains.kotlinx.kover"
],
"enabled" : false
},
{
"matchPackagePatterns" : [
"^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
steps:
- 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
with:
labels: Run-Maestro
- uses: actions/checkout@v4
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
- uses: actions/setup-java@v4
name: Use JDK 17
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Assemble debug APK
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:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- uses: mobile-dev-inc/action-maestro-cloud@v1.8.0
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
# 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
- 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
if: always()
@@ -41,7 +41,7 @@ jobs:
with:
name: kover-results
path: |
**/build/reports/kover/merged
**/build/reports/kover
- name: 🔊 Publish results to Sonar
env:

View File

@@ -26,7 +26,7 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@v1.2.2
with:
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)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@v1.2.2
@@ -43,8 +43,8 @@ jobs:
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots
id: record
run: ./.github/workflows/scripts/recordScreenshots.sh
env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}

View File

@@ -68,11 +68,27 @@ echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn
echo "Committing changes"
git config user.name "ElementBot"
git config user.email "benoitm+elementbot@element.io"
git config http.sslVerify false
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 commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
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!"

View File

@@ -55,7 +55,7 @@ jobs:
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- 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
if: failure()
@@ -63,7 +63,7 @@ jobs:
with:
name: kover-error-report
path: |
**/kover/merged/verification/errors.txt
app/build/reports/kover/verify.err
- name: ✅ Upload kover report (disabled)
if: always()
@@ -83,4 +83,4 @@ jobs:
if: always()
uses: codecov/codecov-action@v3
# 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
- back
- tapOn: "Sign out"
- tapOn:
id: "sign-out-submit"
# Ensure cancel cancels
- tapOn: "Cancel"
- tapOn:
id: "sign-out-submit"
id: "dialog-negative"
- tapOn: "Sign out"
- tapOn:
id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml

View File

@@ -9,5 +9,5 @@ appId: ${APP_ID}
index: 1
- takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2"
- tapOn: "Leave room"
- tapOn: "Leave conversation"
- 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)
========================================

View File

@@ -58,7 +58,6 @@ import java.io.File
*/
@AutoService(CodeGenerator::class)
class ContributesNodeCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true
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.allLibrariesImpl
import extension.allServicesImpl
import org.jetbrains.kotlin.cli.common.toBooleanLenient
plugins {
id("io.element.android-compose-application")
@@ -27,7 +28,8 @@ plugins {
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
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)
id("kotlin-parcelize")
// To be able to update the firebase.xml files, uncomment and build the project
@@ -43,10 +45,6 @@ android {
versionCode = Versions.versionCode
versionName = Versions.versionName
vectorDrawables {
useSupportLibrary = true
}
// Keep abiFilter for the universalApk
ndk {
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 {
allLibrariesImpl()
allServicesImpl()

View File

@@ -10,6 +10,9 @@
-keep class com.sun.jna.** { *; }
-keep class * implements com.sun.jna.** { *; }
# TagSoup, coming from the RTE library
-keep class org.ccil.cowan.tagsoup.** { *; }
# kotlinx.serialization
# Kotlin serialization looks up the generated serializer classes through a function on companion
@@ -32,3 +35,8 @@
-dontwarn org.conscrypt.**
-dontwarn org.bouncycastle.**
-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
class ElementXApplication : Application(), DaggerComponentOwner {
override val daggerComponent: AppComponent = DaggerAppComponent.factory().create(this)
override fun onCreate() {

View File

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

View File

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

View File

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

View File

@@ -28,9 +28,11 @@ import io.element.android.libraries.di.SingleIn
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent : NodeFactoriesBindings {
@Component.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
@ContributesTo(AppScope::class)
object AppModule {
@Provides
fun providesBaseDirectory(@ApplicationContext context: Context): File {
return File(context.filesDir, "sessions")
@@ -82,14 +81,20 @@ object AppModule {
buildType = buildType,
applicationName = context.getString(R.string.app_name),
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,
versionCode = BuildConfig.VERSION_CODE,
gitRevision = "TODO", // BuildConfig.GIT_REVISION,
gitRevisionDate = "TODO", // BuildConfig.GIT_REVISION_DATE,
gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME,
flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION,
flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION,
// BuildConfig.GIT_REVISION,
gitRevision = "TODO",
// BuildConfig.GIT_REVISION_DATE,
gitRevisionDate = "TODO",
// BuildConfig.GIT_BRANCH_NAME,
gitBranchName = "TODO",
// BuildConfig.FLAVOR_DESCRIPTION,
flavorDescription = "TODO",
// BuildConfig.SHORT_FLAVOR_DESCRIPTION,
flavorShortDescription = "TODO",
)
@Provides

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,8 @@ package io.element.android.appconfig
object NotificationConfig {
// 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
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.
*/
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
object RoomListConfig {
const val showInviteMenuItem = false
const val showReportProblemMenuItem = false
const val SHOW_INVITE_MENU_ITEM = 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
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
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
accept(Remove(lastExpectedNavElement.key))
}

View File

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

View File

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

View File

@@ -81,7 +81,6 @@ class RootFlowNode @AssistedInject constructor(
buildContext = buildContext,
plugins = plugins
) {
override fun onBuilt() {
matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap)
super.onBuilt()
@@ -268,10 +267,9 @@ class RootFlowNode @AssistedInject constructor(
}
private suspend fun attachSession(sessionId: SessionId): LoggedInAppScopeFlowNode {
//TODO handle multi-session
// TODO handle multi-session
return waitForChildAttached { navTarget ->
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)
@ContributesBinding(AppScope::class)
class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) : MatrixClientProvider {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>()
private val restoreMutex = Mutex()
@@ -65,8 +64,9 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
@Suppress("UNCHECKED_CAST")
fun restoreWithSavedState(state: SavedStateMap?) {
Timber.d("Restore state")
if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also {
if (state == null || sessionIdsToMatrixClient.isNotEmpty()) {
Timber.w("Restore with non-empty map")
return
}
val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId>
Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}")

View File

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

View File

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

View File

@@ -59,15 +59,15 @@ fun SyncStateView(
) {
Row(
modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp),
.background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(12.dp),
.progressSemantics()
.size(12.dp),
color = ElementTheme.colors.textPrimary,
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.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
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.ui.strings.CommonStrings
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LoadingRoomNodeView(
state: LoadingRoomState,

View File

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

View File

@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
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.plugin.Plugin
@@ -65,7 +66,6 @@ class RoomFlowNode @AssistedInject constructor(
buildContext = buildContext,
plugins = plugins
) {
data class Inputs(
val roomId: RoomId,
val initialElement: RoomLoadedFlowNode.NavTarget = RoomLoadedFlowNode.NavTarget.Messages,
@@ -130,7 +130,8 @@ class RoomFlowNode @AssistedInject constructor(
@Composable
override fun View(modifier: Modifier) {
BackstackView()
BackstackView(
transitionHandler = JumpToEndTransitionHandler(),
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,25 +28,24 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
class LoadingRoomStateFlowFactoryTest {
@Test
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 {
givenGetRoomResult(A_ROOM_ID, room)
}
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
}
.test {
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
}
}
@Test
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 matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)

View File

@@ -1,7 +1,5 @@
import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient
buildscript {
dependencies {
@@ -28,6 +26,7 @@ buildscript {
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("io.element.android-root")
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
@@ -41,7 +40,6 @@ plugins {
alias(libs.plugins.ktlint)
alias(libs.plugins.dependencygraph)
alias(libs.plugins.sonarqube)
alias(libs.plugins.kover)
}
tasks.register<Delete>("clean").configure {
@@ -62,7 +60,7 @@ allprojects {
config.from(files("$rootDir/tools/detekt/detekt.yml"))
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:0.3.8")
detektPlugins("io.nlopez.compose.rules:detekt:0.3.9")
}
// KtLint
@@ -74,7 +72,7 @@ allprojects {
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
// See https://github.com/pinterest/ktlint/releases/
// TODO Regularly check for new version here ^
version.set("0.48.2")
version.set("1.1.1")
android.set(true)
ignoreFailures.set(false)
enableExperimentalRules.set(true)
@@ -100,11 +98,16 @@ allprojects {
// You can override by passing `-PallWarningsAsErrors=true` in the command line
// Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
}
// Detect unused dependencies
apply {
plugin("com.autonomousapps.dependency-analysis")
kotlinOptions {
/*
// 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.
tasks.register("runQualityChecks") {
project.subprojects {
@@ -361,7 +190,7 @@ subprojects {
// Workaround for https://github.com/airbnb/Showkase/issues/335
subprojects {
tasks.withType<KspTask>() {
tasks.withType<KspTask> {
doLast {
fileTree(layout.buildDirectory).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
ReplaceRegExp().apply {
@@ -383,13 +212,15 @@ subprojects {
if (project.findProperty("composeCompilerReports") == "true") {
freeCompilerArgs += listOf(
"-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") {
freeCompilerArgs += listOf(
"-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
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:
```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
```bash
./gradlew koverMergedVerify
./gradlew :app:koverVerify
```
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
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

View File

@@ -8,7 +8,7 @@
* [Stop Synapse](#stop-synapse)
* [Troubleshoot](#troubleshoot)
* [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)
<!--- 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.libraries.designsystem.components.LINK_TAG
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.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.ListSupportingText

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"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_analytics_settings_share_data">"分享分析數據"</string>
<string name="screen_analytics_settings_help_us_improve">"分享匿名的使用數據以協助我們釐清問題。"</string>
<string name="screen_analytics_settings_share_data">"提供分析數據"</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_content_link">"這裡"</string>
</resources>

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ class DefaultAnalyticsPreferencesPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
private val buildMeta: BuildMeta,
) : AnalyticsPreferencesPresenter {
@Composable
override fun present(): AnalyticsPreferencesState {
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"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<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_content_link">"這裡"</string>
<string name="screen_analytics_prompt_settings">"您可以在任何時候關閉它"</string>

View File

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

View File

@@ -29,7 +29,6 @@ import org.junit.Rule
import org.junit.Test
class AnalyticsPreferencesPresenterTest {
@get:Rule
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
class CallForegroundService : Service() {
companion object {
fun start(context: Context) {
val intent = Intent(context, CallForegroundService::class.java)

View File

@@ -28,11 +28,11 @@ data class WidgetMessage(
@SerialName("action") val action: Action,
@SerialName("data") val data: JsonElement? = null,
) {
@Serializable
enum class Direction {
@SerialName("fromWidget")
FromWidget,
@SerialName("toWidget")
ToWidget
}
@@ -41,6 +41,7 @@ data class WidgetMessage(
enum class Action {
@SerialName("im.vector.hangup")
HangUp,
@SerialName("send_event")
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.WidgetMessageInterceptor
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.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
@@ -63,7 +63,6 @@ class CallScreenPresenter @AssistedInject constructor(
private val matrixClientsProvider: MatrixClientProvider,
private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
fun create(callType: CallType, navigator: CallScreenNavigator): CallScreenPresenter
@@ -75,7 +74,7 @@ class CallScreenPresenter @AssistedInject constructor(
@Composable
override fun present(): CallScreenState {
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 messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) }
@@ -154,7 +153,7 @@ class CallScreenPresenter @AssistedInject constructor(
private fun CoroutineScope.loadUrl(
inputs: CallType,
urlState: MutableState<Async<String>>,
urlState: MutableState<AsyncData<String>>,
callWidgetDriver: MutableState<MatrixWidgetDriver?>,
) = launch {
urlState.runCatchingUpdatingState {
@@ -224,6 +223,4 @@ class CallScreenPresenter @AssistedInject constructor(
navigator.close()
widgetDriver?.close()
}
}

View File

@@ -16,10 +16,10 @@
package io.element.android.features.call.ui
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.AsyncData
data class CallScreenState(
val urlState: Async<String>,
val urlState: AsyncData<String>,
val userAgent: String,
val isInWidgetMode: Boolean,
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.features.call.R
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.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -96,7 +96,7 @@ internal fun CallScreenView(
@Composable
private fun CallWebView(
url: Async<String>,
url: AsyncData<String>,
userAgent: String,
onPermissionsRequested: (PermissionRequest) -> Unit,
onWebViewCreated: (WebView) -> Unit,
@@ -116,7 +116,7 @@ private fun CallWebView(
}
},
update = { webView ->
if (url is Async.Success && webView.url != url.data) {
if (url is AsyncData.Success && webView.url != url.data) {
webView.loadUrl(url.data)
}
},
@@ -161,7 +161,7 @@ internal fun CallScreenViewPreview() {
ElementPreview {
CallScreenView(
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,
userAgent = "",
eventSink = {},

View File

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

View File

@@ -28,7 +28,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
class WebViewWidgetMessageInterceptor(
private val webView: WebView,
) : WidgetMessageInterceptor {
companion object {
// 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
@@ -56,7 +55,7 @@ class WebViewWidgetMessageInterceptor(
|| !message.data.response && message.data.api == "fromWidget") {
let json = JSON.stringify(event.data)
${"console.log('message sent: ' + json);".takeIf { BuildConfig.DEBUG } }
${LISTENER_NAME}.postMessage(json);
$LISTENER_NAME.postMessage(json);
} else {
${"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
object WidgetMessageSerializer {
private val coder = Json { ignoreUnknownKeys = true }
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"?>
<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_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>
</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">
<string name="call_foreground_service_channel_title_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>

View File

@@ -23,7 +23,6 @@ import io.element.android.features.call.ui.mapWebkitPermissions
import org.junit.Test
class MapWebkitPermissionsTest {
@Test
fun `given Webkit's RESOURCE_AUDIO_CAPTURE returns Android's RECORD_AUDIO permission`() {
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.utils.FakeCallWidgetProvider
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.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.test.A_ROOM_ID
@@ -48,7 +48,6 @@ import org.junit.Rule
import org.junit.Test
class CallScreenPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -62,7 +61,7 @@ class CallScreenPresenterTest {
skipItems(1)
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()
}
}
@@ -83,7 +82,7 @@ class CallScreenPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(Async.Success::class.java)
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
@@ -191,7 +190,7 @@ class CallScreenPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
consumeItemsUntilTimeout()
consumeItemsUntilTimeout()
assertThat(matrixClient.syncService().syncState.value).isEqualTo(SyncState.Running)

View File

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

View File

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

View File

@@ -32,7 +32,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
class DefaultCallWidgetProviderTest {
@Test
fun `getWidget - fails if the session does not exist`() = runTest {
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(
private val widgetDriver: FakeWidgetDriver = FakeWidgetDriver(),
private val url: String = "https://call.element.io",
) : CallWidgetProvider {
) : CallWidgetProvider {
var getWidgetCalled = false
private set
var getWidgetCalled = false
private set
override suspend fun getWidget(
sessionId: SessionId,
roomId: RoomId,
clientId: String,
languageTag: String?,
theme: String?
): Result<Pair<MatrixWidgetDriver, String>> {
getWidgetCalled = true
return Result.success(widgetDriver to url)
}
override suspend fun getWidget(
sessionId: SessionId,
roomId: RoomId,
clientId: String,
languageTag: String?,
theme: String?
): 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
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) {
sentMessages += message
}
fun givenInterceptedMessage(message: String) {
interceptedMessages.tryEmit(message)
}
override fun sendMessage(message: String) {
sentMessages += 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
interface CreateRoomEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder

View File

@@ -17,7 +17,7 @@
package io.element.android.features.createroom.api
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.UserId
@@ -27,5 +27,5 @@ interface StartDMAction {
* @param userId The user to start a DM with.
* @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,
plugins = plugins
) {
private val component by lazy {
parent!!.bindings<CreateRoomComponent.ParentBindings>().createRoomComponentBuilder().build()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,14 +29,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
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.roomPrivacyItems
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
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.RadioButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.compound.theme.ElementTheme
@Composable
fun RoomPrivacyOption(
@@ -85,7 +85,8 @@ fun RoomPrivacyOption(
.align(Alignment.CenterVertically)
.size(48.dp),
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
@Composable
internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview { ContentToPreview() }
@Composable
private fun ContentToPreview() {
internal fun SearchMultipleUsersResultItemPreview() = ElementThemedPreview {
Column {
SearchMultipleUsersResultItem(
searchResult = UserSearchResult(

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