Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
9318d27ba1
154 changed files with 2161 additions and 832 deletions
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
|
|
@ -4,7 +4,6 @@
|
||||||
"config:base"
|
"config:base"
|
||||||
],
|
],
|
||||||
"labels": ["dependencies"],
|
"labels": ["dependencies"],
|
||||||
"reviewers": ["team:element-x-android-reviewers"],
|
|
||||||
"ignoreDeps": ["string:app_name"],
|
"ignoreDeps": ["string:app_name"],
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
1
.github/workflows/nightly.yml
vendored
1
.github/workflows/nightly.yml
vendored
|
|
@ -1,6 +1,7 @@
|
||||||
name: Build and release nightly APK
|
name: Build and release nightly APK
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
# Every nights at 4
|
# Every nights at 4
|
||||||
- cron: "0 4 * * *"
|
- cron: "0 4 * * *"
|
||||||
|
|
|
||||||
43
.github/workflows/nightly_manual.yml
vendored
43
.github/workflows/nightly_manual.yml
vendored
|
|
@ -1,43 +0,0 @@
|
||||||
name: Build and release nightly APK manually
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch
|
|
||||||
|
|
||||||
env:
|
|
||||||
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false
|
|
||||||
CI_GRADLE_ARG_PROPERTIES: --stacktrace -PpreDexEnable=false --max-workers 2 --no-daemon
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
nightly:
|
|
||||||
name: Build and publish nightly APK to Firebase
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Use JDK 17
|
|
||||||
uses: actions/setup-java@v3
|
|
||||||
with:
|
|
||||||
distribution: 'temurin' # See 'Supported distributions' for available options
|
|
||||||
java-version: '17'
|
|
||||||
- name: Install towncrier
|
|
||||||
run: |
|
|
||||||
python3 -m pip install towncrier
|
|
||||||
- name: Prepare changelog file
|
|
||||||
run: |
|
|
||||||
mv towncrier.toml towncrier.toml.bak
|
|
||||||
sed 's/CHANGES\.md/CHANGES_NIGHTLY\.md/' towncrier.toml.bak > towncrier.toml
|
|
||||||
rm towncrier.toml.bak
|
|
||||||
yes n | towncrier build --version nightly
|
|
||||||
- name: Build and upload Nightly APK
|
|
||||||
run: |
|
|
||||||
./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES
|
|
||||||
env:
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }}
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }}
|
|
||||||
ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }}
|
|
||||||
FIREBASE_TOKEN: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_FIREBASE_TOKEN }}
|
|
||||||
- name: Additionally upload Nightly APK to browserstack for testing
|
|
||||||
continue-on-error: true # don't block anything by this upload failing (for now)
|
|
||||||
run: curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_PASSWORD" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@app/build/outputs/apk/nightly/app-universal-nightly.apk" -F "custom_id=element-x-android-nightly"
|
|
||||||
env:
|
|
||||||
BROWSERSTACK_USERNAME: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_USERNAME }}
|
|
||||||
BROWSERSTACK_PASSWORD: ${{ secrets.ELEMENT_ANDROID_BROWSERSTACK_ACCESS_KEY }}
|
|
||||||
5
.maestro/tests/assertions/assertRoomListSynced.yaml
Normal file
5
.maestro/tests/assertions/assertRoomListSynced.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
appId: ${APP_ID}
|
||||||
|
---
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible: ${ROOM_NAME}
|
||||||
|
timeout: 10_000
|
||||||
|
|
@ -3,6 +3,5 @@ appId: ${APP_ID}
|
||||||
- clearState
|
- clearState
|
||||||
- launchApp:
|
- launchApp:
|
||||||
clearKeychain: true
|
clearKeychain: true
|
||||||
- tapOn: "Close showkase button"
|
|
||||||
- runFlow: ./assertions/assertInitDisplayed.yaml
|
- runFlow: ./assertions/assertInitDisplayed.yaml
|
||||||
- takeScreenshot: build/maestro/000-FirstScreen
|
- takeScreenshot: build/maestro/000-FirstScreen
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
appId: ${APP_ID}
|
appId: ${APP_ID}
|
||||||
---
|
---
|
||||||
|
- runFlow: ../assertions/assertRoomListSynced.yaml
|
||||||
- tapOn: "search"
|
- tapOn: "search"
|
||||||
- inputText: ${ROOM_NAME.substring(0, 3)}
|
- inputText: ${ROOM_NAME.substring(0, 3)}
|
||||||
- takeScreenshot: build/maestro/400-SearchRoom
|
- takeScreenshot: build/maestro/400-SearchRoom
|
||||||
|
|
|
||||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* @vector-im/element-x-android-reviewers
|
||||||
|
|
@ -23,7 +23,7 @@ dependencies {
|
||||||
implementation(projects.anvilannotations)
|
implementation(projects.anvilannotations)
|
||||||
api(libs.anvil.compiler.api)
|
api(libs.anvil.compiler.api)
|
||||||
implementation(libs.anvil.compiler.utils)
|
implementation(libs.anvil.compiler.utils)
|
||||||
implementation("com.squareup:kotlinpoet:1.13.1")
|
implementation("com.squareup:kotlinpoet:1.13.2")
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0.1")
|
compileOnly("com.google.auto.service:auto-service-annotations:1.0.1")
|
||||||
kapt("com.google.auto.service:auto-service:1.0.1")
|
kapt("com.google.auto.service:auto-service:1.0.1")
|
||||||
|
|
|
||||||
|
|
@ -29,15 +29,6 @@
|
||||||
android:theme="@style/Theme.ElementX"
|
android:theme="@style/Theme.ElementX"
|
||||||
tools:targetApi="33">
|
tools:targetApi="33">
|
||||||
|
|
||||||
<provider
|
|
||||||
android:authorities="${applicationId}.fileprovider"
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:grantUriPermissions="true"
|
|
||||||
android:exported="false">
|
|
||||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_providers" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||||
|
|
@ -66,6 +57,15 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_providers" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumble.appyx.core.composable.Children
|
import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -53,7 +51,6 @@ import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.tests.uitests.openShowkase
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -140,17 +137,11 @@ class RootFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val activity = LocalContext.current as Activity
|
|
||||||
fun openShowkase() {
|
|
||||||
openShowkase(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
RootView(
|
RootView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onOpenBugReport = this::onOpenBugReport,
|
onOpenBugReport = this::onOpenBugReport,
|
||||||
onOpenShowkase = ::openShowkase
|
|
||||||
) {
|
) {
|
||||||
Children(
|
Children(
|
||||||
navModel = backstack,
|
navModel = backstack,
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,6 @@
|
||||||
package io.element.android.appnav.root
|
package io.element.android.appnav.root
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
|
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
|
||||||
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
|
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
@ -31,23 +29,12 @@ class RootPresenter @Inject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RootState {
|
override fun present(): RootState {
|
||||||
val isShowkaseButtonVisible = rememberSaveable {
|
|
||||||
mutableStateOf(true)
|
|
||||||
}
|
|
||||||
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
|
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
|
||||||
val crashDetectionState = crashDetectionPresenter.present()
|
val crashDetectionState = crashDetectionPresenter.present()
|
||||||
|
|
||||||
fun handleEvent(event: RootEvents) {
|
|
||||||
when (event) {
|
|
||||||
RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return RootState(
|
return RootState(
|
||||||
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
|
|
||||||
rageshakeDetectionState = rageshakeDetectionState,
|
rageshakeDetectionState = rageshakeDetectionState,
|
||||||
crashDetectionState = crashDetectionState,
|
crashDetectionState = crashDetectionState,
|
||||||
eventSink = ::handleEvent
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionSta
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class RootState(
|
data class RootState(
|
||||||
val isShowkaseButtonVisible: Boolean,
|
|
||||||
val rageshakeDetectionState: RageshakeDetectionState,
|
val rageshakeDetectionState: RageshakeDetectionState,
|
||||||
val crashDetectionState: CrashDetectionState,
|
val crashDetectionState: CrashDetectionState,
|
||||||
val eventSink: (RootEvents) -> Unit
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,10 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
|
||||||
override val values: Sequence<RootState>
|
override val values: Sequence<RootState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRootState().copy(
|
aRootState().copy(
|
||||||
isShowkaseButtonVisible = true,
|
|
||||||
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = false),
|
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = false),
|
||||||
crashDetectionState = aCrashDetectionState().copy(crashDetected = true),
|
crashDetectionState = aCrashDetectionState().copy(crashDetected = true),
|
||||||
),
|
),
|
||||||
aRootState().copy(
|
aRootState().copy(
|
||||||
isShowkaseButtonVisible = true,
|
|
||||||
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true),
|
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true),
|
||||||
crashDetectionState = aCrashDetectionState().copy(crashDetected = false),
|
crashDetectionState = aCrashDetectionState().copy(crashDetected = false),
|
||||||
)
|
)
|
||||||
|
|
@ -37,8 +35,6 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aRootState() = RootState(
|
fun aRootState() = RootState(
|
||||||
isShowkaseButtonVisible = false,
|
|
||||||
rageshakeDetectionState = aRageshakeDetectionState(),
|
rageshakeDetectionState = aRageshakeDetectionState(),
|
||||||
crashDetectionState = aCrashDetectionState(),
|
crashDetectionState = aCrashDetectionState(),
|
||||||
eventSink = {}
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ fun RootView(
|
||||||
state: RootState,
|
state: RootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onOpenBugReport: () -> Unit = {},
|
onOpenBugReport: () -> Unit = {},
|
||||||
onOpenShowkase: () -> Unit = {},
|
|
||||||
children: @Composable BoxScope.() -> Unit,
|
children: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -46,7 +45,6 @@ fun RootView(
|
||||||
contentAlignment = Alignment.TopCenter,
|
contentAlignment = Alignment.TopCenter,
|
||||||
) {
|
) {
|
||||||
children()
|
children()
|
||||||
val eventSink = state.eventSink
|
|
||||||
|
|
||||||
fun onOpenBugReport() {
|
fun onOpenBugReport() {
|
||||||
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
||||||
|
|
@ -54,11 +52,6 @@ fun RootView(
|
||||||
onOpenBugReport.invoke()
|
onOpenBugReport.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
ShowkaseButton(
|
|
||||||
isVisible = state.isShowkaseButtonVisible,
|
|
||||||
onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
|
|
||||||
onClick = onOpenShowkase
|
|
||||||
)
|
|
||||||
RageshakeDetectionView(
|
RageshakeDetectionView(
|
||||||
state = state.rageshakeDetectionState,
|
state = state.rageshakeDetectionState,
|
||||||
onOpenBugReport = ::onOpenBugReport,
|
onOpenBugReport = ::onOpenBugReport,
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.appnav.root
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Button
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.IconButton
|
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun ShowkaseButton(
|
|
||||||
isVisible: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onClick: () -> Unit = {},
|
|
||||||
onCloseClicked: () -> Unit = {},
|
|
||||||
) {
|
|
||||||
if (isVisible) {
|
|
||||||
Button(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(top = 32.dp),
|
|
||||||
onClick = onClick
|
|
||||||
) {
|
|
||||||
Text(text = "Showkase Browser")
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 8.dp)
|
|
||||||
.size(16.dp),
|
|
||||||
onClick = onCloseClicked,
|
|
||||||
) {
|
|
||||||
Icon(imageVector = Icons.Filled.Close, contentDescription = "Close showkase button")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
internal fun ShowkaseButtonLightPreview() = ElementPreviewLight { ContentToPreview() }
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
internal fun ShowkaseButtonDarkPreview() = ElementPreviewDark { ContentToPreview() }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ContentToPreview() {
|
|
||||||
ShowkaseButton(isVisible = true)
|
|
||||||
}
|
|
||||||
|
|
@ -14,15 +14,12 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.appnav
|
package io.element.android.appnav
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.root.RootEvents
|
|
||||||
import io.element.android.appnav.root.RootPresenter
|
import io.element.android.appnav.root.RootPresenter
|
||||||
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
||||||
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter
|
import io.element.android.features.rageshake.impl.detection.DefaultRageshakeDetectionPresenter
|
||||||
|
|
@ -31,7 +28,6 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
@ -44,21 +40,7 @@ class RootPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.isShowkaseButtonVisible).isTrue()
|
assertThat(initialState.crashDetectionState.crashDetected).isFalse()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - hide showkase button`() = runTest {
|
|
||||||
val presenter = createPresenter()
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
skipItems(1)
|
|
||||||
val initialState = awaitItem()
|
|
||||||
assertThat(initialState.isShowkaseButtonVisible).isTrue()
|
|
||||||
initialState.eventSink.invoke(RootEvents.HideShowkaseButton)
|
|
||||||
assertThat(awaitItem().isShowkaseButtonVisible).isFalse()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.appnav.loggedin
|
package io.element.android.appnav.loggedin
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -29,7 +27,6 @@ import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
|
||||||
import io.element.android.libraries.push.api.PushService
|
import io.element.android.libraries.push.api.PushService
|
||||||
import io.element.android.libraries.push.providers.api.Distributor
|
import io.element.android.libraries.push.providers.api.Distributor
|
||||||
import io.element.android.libraries.push.providers.api.PushProvider
|
import io.element.android.libraries.push.providers.api.PushProvider
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -285,12 +285,15 @@ tasks.register("runQualityChecks") {
|
||||||
|
|
||||||
// Make sure to delete old screenshots before recording new ones
|
// Make sure to delete old screenshots before recording new ones
|
||||||
subprojects {
|
subprojects {
|
||||||
val snapshotsDir = File("${project.path}/src/test/snapshots")
|
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")
|
||||||
val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") {
|
val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") {
|
||||||
onlyIf { snapshotsDir.exists() }
|
onlyIf { snapshotsDir.exists() }
|
||||||
doFirst {
|
doFirst {
|
||||||
|
println("Delete previous screenshots located at $snapshotsDir\n")
|
||||||
snapshotsDir.deleteRecursively()
|
snapshotsDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tasks.findByName("recordPaparazzi")?.dependsOn(removeOldScreenshotsTask)
|
tasks.findByName("recordPaparazzi")?.dependsOn(removeOldScreenshotsTask)
|
||||||
|
tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
|
||||||
|
tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
changelog.d/385.feature
Normal file
1
changelog.d/385.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Show pending invitations in room members list
|
||||||
1
changelog.d/396.feature
Normal file
1
changelog.d/396.feature
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Add media pre-processing before uploading it.
|
||||||
|
|
@ -30,11 +30,16 @@ If installed correctly, `git push` and `git pull` will now include LFS content.
|
||||||
|
|
||||||
## Recording
|
## Recording
|
||||||
|
|
||||||
It's recommended to delete the content of the folder `/snapshots` before recording.
|
```shell
|
||||||
|
./gradlew recordPaparazziDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
The task will delete the content of the folder `/snapshots` before recording (see the task `removeOldSnapshots` defined in the project).
|
||||||
|
|
||||||
|
If this is not the case, you can run
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
rm -rf ./tests/uitests/src/test/snapshots
|
rm -rf ./tests/uitests/src/test/snapshots
|
||||||
./gradlew recordPaparazziDebug
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Paparazzi will generate images in `:tests:uitests/src/test/snapshots`, which will need to be committed to the repository using Git LFS.
|
Paparazzi will generate images in `:tests:uitests/src/test/snapshots`, which will need to be committed to the repository using Git LFS.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import com.bumble.appyx.core.composable.Children
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
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
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -76,10 +77,11 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
||||||
backstack.push(NavTarget.ConfigureRoom)
|
backstack.push(NavTarget.ConfigureRoom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createNode<AddPeopleNode>(context = buildContext, plugins = plugins.plus(callback))
|
createNode<AddPeopleNode>(context = buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
NavTarget.ConfigureRoom -> {
|
NavTarget.ConfigureRoom -> {
|
||||||
createNode<ConfigureRoomNode>(context = buildContext, plugins = plugins)
|
val callbacks = plugins<ConfigureRoomNode.Callback>()
|
||||||
|
createNode<ConfigureRoomNode>(context = buildContext, plugins = callbacks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,8 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
fun onCreateRoomSuccess(roomId: RoomId)
|
fun onCreateRoomSuccess(roomId: RoomId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callback = object : Callback {
|
private fun onRoomCreated(roomId: RoomId) {
|
||||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
||||||
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -52,7 +50,7 @@ class ConfigureRoomNode @AssistedInject constructor(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = this::navigateUp,
|
onBackPressed = this::navigateUp,
|
||||||
onRoomCreated = callback::onCreateRoomSuccess
|
onRoomCreated = this::onRoomCreated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,11 +65,12 @@ class CreateRoomRootPresenter @Inject constructor(
|
||||||
|
|
||||||
fun startDm(matrixUser: MatrixUser) {
|
fun startDm(matrixUser: MatrixUser) {
|
||||||
startDmAction.value = Async.Uninitialized
|
startDmAction.value = Async.Uninitialized
|
||||||
val existingDM = matrixClient.findDM(matrixUser.userId)
|
matrixClient.findDM(matrixUser.userId).use { existingDM ->
|
||||||
if (existingDM == null) {
|
if (existingDM == null) {
|
||||||
localCoroutineScope.createDM(matrixUser, startDmAction)
|
localCoroutineScope.createDM(matrixUser, startDmAction)
|
||||||
} else {
|
} else {
|
||||||
startDmAction.value = Async.Success(existingDM.roomId)
|
startDmAction.value = Async.Success(existingDM.roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,5 @@
|
||||||
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
<string name="screen_create_room_add_people_title">"Añadir personas"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
<string name="screen_start_chat_error_starting_chat">"Se ha producido un error al intentar iniciar un chat"</string>
|
||||||
<string name="screen_start_chat_unknown_profile">"No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación."</string>
|
<string name="screen_start_chat_unknown_profile">"No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación."</string>
|
||||||
|
<string name="screen_create_room_title">"Crear una sala"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -5,4 +5,5 @@
|
||||||
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
<string name="screen_create_room_add_people_title">"Aggiungi persone"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
<string name="screen_start_chat_error_starting_chat">"Si è verificato un errore durante il tentativo di avviare una chat"</string>
|
||||||
<string name="screen_start_chat_unknown_profile">"Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto."</string>
|
<string name="screen_start_chat_unknown_profile">"Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto."</string>
|
||||||
|
<string name="screen_create_room_title">"Crea una stanza"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
|
<string name="screen_create_room_public_option_title">"Cameră publică (oricine)"</string>
|
||||||
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
|
<string name="screen_create_room_room_name_label">"Numele camerei"</string>
|
||||||
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
|
<string name="screen_create_room_room_name_placeholder">"e.g. Mici și Cozonaci"</string>
|
||||||
<string name="screen_create_room_title">"Creați o cameră"</string>
|
|
||||||
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
|
<string name="screen_create_room_topic_label">"Subiect (opțional)"</string>
|
||||||
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
|
<string name="screen_create_room_topic_placeholder">"Despre ce este această cameră?"</string>
|
||||||
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
<string name="screen_start_chat_error_starting_chat">"A apărut o eroare la încercarea începerii conversației"</string>
|
||||||
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
<string name="screen_start_chat_unknown_profile">"Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită."</string>
|
||||||
|
<string name="screen_create_room_title">"Creați o cameră"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -25,11 +25,9 @@ import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class AllMatrixUsersDataSourceTest {
|
internal class AllMatrixUsersDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.addpeople
|
package io.element.android.features.createroom.impl.addpeople
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -26,7 +24,6 @@ import io.element.android.features.createroom.impl.CreateRoomDataStore
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.userlist.api.UserListDataStore
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.configureroom
|
package io.element.android.features.createroom.impl.configureroom
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
|
@ -36,7 +34,6 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.createroom.impl.root
|
package io.element.android.features.createroom.impl.root
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -35,7 +33,6 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
|
||||||
|
|
@ -131,14 +131,18 @@ class InviteListPresenter @Inject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
|
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
client.getRoom(roomId)?.acceptInvitation()?.getOrThrow()
|
client.getRoom(roomId)?.use {
|
||||||
|
it.acceptInvitation().getOrThrow()
|
||||||
|
}
|
||||||
roomId
|
roomId
|
||||||
}.execute(acceptedAction)
|
}.execute(acceptedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||||
suspend {
|
suspend {
|
||||||
client.getRoom(roomId)?.rejectInvitation()?.getOrThrow() ?: Unit
|
client.getRoom(roomId)?.use {
|
||||||
|
it.rejectInvitation().getOrThrow()
|
||||||
|
} ?: Unit
|
||||||
}.execute(declinedAction)
|
}.execute(declinedAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,9 @@ import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class InviteListPresenterTests {
|
class InviteListPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -509,5 +507,4 @@ class InviteListPresenterTests {
|
||||||
unreadNotificationCount = 0,
|
unreadNotificationCount = 0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeserver
|
package io.element.android.features.login.impl.changeserver
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -28,11 +26,9 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class ChangeServerPresenterTest {
|
class ChangeServerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with default homeserver`() = runTest {
|
fun `present - should start with default homeserver`() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.root
|
package io.element.android.features.login.impl.root
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.impl.root.LoggedInState
|
|
||||||
import io.element.android.features.login.impl.root.LoginFormState
|
|
||||||
import io.element.android.features.login.impl.root.LoginRootEvents
|
|
||||||
import io.element.android.features.login.impl.root.LoginRootPresenter
|
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||||
import io.element.android.libraries.matrix.test.A_PASSWORD
|
import io.element.android.libraries.matrix.test.A_PASSWORD
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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_signout_confirmation_dialog_submit">"Abmelden"</string>
|
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Abmelden"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Abmeldung läuft…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Abmeldung läuft…"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_submit">"Abmelden"</string>
|
||||||
<string name="screen_signout_preference_item">"Abmelden"</string>
|
<string name="screen_signout_preference_item">"Abmelden"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?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_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"¿Estás seguro de que quieres cerrar sesión?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
|
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Cerrar sesión"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Cerrando sesión…"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_submit">"Cerrar sesión"</string>
|
||||||
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
|
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?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_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Sei sicuro di voler uscire?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Esci"</string>
|
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Esci"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Esci"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Uscita in corso…"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_submit">"Esci"</string>
|
||||||
<string name="screen_signout_preference_item">"Esci"</string>
|
<string name="screen_signout_preference_item">"Esci"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<?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_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
|
<string name="screen_signout_confirmation_dialog_content">"Sunteți sigur că vreți să vă deconectați?"</string>
|
||||||
<string name="screen_signout_confirmation_dialog_submit">"Deconectați-vă"</string>
|
|
||||||
<string name="screen_signout_confirmation_dialog_title">"Deconectați-vă"</string>
|
<string name="screen_signout_confirmation_dialog_title">"Deconectați-vă"</string>
|
||||||
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
|
<string name="screen_signout_in_progress_dialog_content">"Deconectare în curs…"</string>
|
||||||
|
<string name="screen_signout_confirmation_dialog_submit">"Deconectați-vă"</string>
|
||||||
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
|
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.logout.impl
|
package io.element.android.features.logout.impl
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -28,7 +26,6 @@ import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.A_THROWABLE
|
import io.element.android.libraries.matrix.test.A_THROWABLE
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,9 @@ dependencies {
|
||||||
implementation(projects.libraries.textcomposer)
|
implementation(projects.libraries.textcomposer)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.dateformatter.api)
|
implementation(projects.libraries.dateformatter.api)
|
||||||
implementation(projects.libraries.mediapickers)
|
implementation(projects.libraries.mediapickers.api)
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
|
implementation(projects.libraries.mediaupload.api)
|
||||||
implementation(projects.features.networkmonitor.api)
|
implementation(projects.features.networkmonitor.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
@ -64,6 +65,9 @@ dependencies {
|
||||||
testImplementation(projects.features.networkmonitor.test)
|
testImplementation(projects.features.networkmonitor.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.libraries.featureflag.test)
|
testImplementation(projects.libraries.featureflag.test)
|
||||||
|
testImplementation(projects.libraries.mediaupload.test)
|
||||||
|
testImplementation(projects.libraries.mediapickers.test)
|
||||||
|
testImplementation(libs.test.mockk)
|
||||||
|
|
||||||
androidTestImplementation(libs.test.junitext)
|
androidTestImplementation(libs.test.junitext)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.designsystem.utils.handleSnackbarMessage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -52,6 +54,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
private val timelinePresenter: TimelinePresenter,
|
private val timelinePresenter: TimelinePresenter,
|
||||||
private val actionListPresenter: ActionListPresenter,
|
private val actionListPresenter: ActionListPresenter,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<MessagesState> {
|
) : Presenter<MessagesState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -71,6 +74,8 @@ class MessagesPresenter @Inject constructor(
|
||||||
|
|
||||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
|
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
|
||||||
|
|
||||||
|
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||||
|
|
||||||
LaunchedEffect(syncUpdateFlow) {
|
LaunchedEffect(syncUpdateFlow) {
|
||||||
roomAvatar.value =
|
roomAvatar.value =
|
||||||
AvatarData(
|
AvatarData(
|
||||||
|
|
@ -97,6 +102,7 @@ class MessagesPresenter @Inject constructor(
|
||||||
timelineState = timelineState,
|
timelineState = timelineState,
|
||||||
actionListState = actionListState,
|
actionListState = actionListState,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
|
snackbarMessage = snackbarMessage,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -32,5 +33,6 @@ data class MessagesState(
|
||||||
val timelineState: TimelineState,
|
val timelineState: TimelineState,
|
||||||
val actionListState: ActionListState,
|
val actionListState: ActionListState,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
|
val snackbarMessage: SnackbarMessage?,
|
||||||
val eventSink: (MessagesEvents) -> Unit
|
val eventSink: (MessagesEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,6 @@ fun aMessagesState() = MessagesState(
|
||||||
),
|
),
|
||||||
actionListState = anActionListState(),
|
actionListState = anActionListState(),
|
||||||
hasNetworkConnection = true,
|
hasNetworkConnection = true,
|
||||||
|
snackbarMessage = null,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -99,7 +100,6 @@ fun MessagesView(
|
||||||
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
||||||
initialValue = ModalBottomSheetValue.Hidden,
|
initialValue = ModalBottomSheetValue.Hidden,
|
||||||
)
|
)
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
|
||||||
val composerState = state.composerState
|
val composerState = state.composerState
|
||||||
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) {
|
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) {
|
||||||
ModalBottomSheetValue.Expanded
|
ModalBottomSheetValue.Expanded
|
||||||
|
|
@ -122,6 +122,19 @@ fun MessagesView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val snackbarMessageText = state.snackbarMessage?.let { stringResource(it.messageResId) }
|
||||||
|
if (snackbarMessageText != null) {
|
||||||
|
SideEffect {
|
||||||
|
coroutineScope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = snackbarMessageText,
|
||||||
|
duration = state.snackbarMessage.duration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
|
// This is needed because the composer is inside an AndroidView that can't be affected by the FocusManager in Compose
|
||||||
val localView = LocalView.current
|
val localView = LocalView.current
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,23 +27,30 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.media3.common.MimeTypes
|
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
import io.element.android.features.messages.impl.media.local.LocalMediaFactory
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.data.StableCharSequence
|
import io.element.android.libraries.core.data.StableCharSequence
|
||||||
import io.element.android.libraries.core.data.toStableCharSequence
|
import io.element.android.libraries.core.data.toStableCharSequence
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarMessage
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.mediapickers.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaType
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@SingleIn(RoomScope::class)
|
@SingleIn(RoomScope::class)
|
||||||
class MessageComposerPresenter @Inject constructor(
|
class MessageComposerPresenter @Inject constructor(
|
||||||
|
|
@ -52,6 +59,8 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
private val mediaPickerProvider: PickerProvider,
|
private val mediaPickerProvider: PickerProvider,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val localMediaFactory: LocalMediaFactory,
|
private val localMediaFactory: LocalMediaFactory,
|
||||||
|
private val mediaPreProcessor: MediaPreProcessor,
|
||||||
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : Presenter<MessageComposerState> {
|
) : Presenter<MessageComposerState> {
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
|
@ -73,10 +82,12 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { handlePickedMedia(it) })
|
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType ->
|
||||||
val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { handlePickedMedia(it) })
|
handlePickedMedia(uri, mimeType)
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) }, deleteAfter = false)
|
})
|
||||||
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) }, deleteAfter = false)
|
val filesPicker = mediaPickerProvider.registerFilePicker(MimeTypes.Any, onResult = { handlePickedMedia(it) })
|
||||||
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.Jpeg) })
|
||||||
|
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.Mp4) })
|
||||||
|
|
||||||
val isFullScreen = rememberSaveable {
|
val isFullScreen = rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
|
@ -174,4 +185,45 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.sendMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mediaType: MediaType,
|
||||||
|
deleteOriginal: Boolean = false
|
||||||
|
) = launch {
|
||||||
|
runCatching {
|
||||||
|
val info = handleMediaPreProcessing(uri, mediaType, deleteOriginal).getOrNull() ?: return@runCatching
|
||||||
|
when (info) {
|
||||||
|
is MediaUploadInfo.Image -> {
|
||||||
|
room.sendImage(info.file, info.thumbnailInfo.file, info.info)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaUploadInfo.Video -> {
|
||||||
|
room.sendVideo(info.file, info.thumbnailInfo.file, info.info)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MediaUploadInfo.AnyFile -> {
|
||||||
|
room.sendFile(info.file, info.info)
|
||||||
|
}
|
||||||
|
else -> error("Unexpected MediaUploadInfo format: $info")
|
||||||
|
}.getOrThrow()
|
||||||
|
}.onFailure {
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(StringR.string.screen_media_upload_preview_error_failed_sending))
|
||||||
|
Timber.e(it, "Couldn't upload media")
|
||||||
|
}.onSuccess {
|
||||||
|
Timber.d("Media uploaded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleMediaPreProcessing(
|
||||||
|
uri: Uri,
|
||||||
|
mediaType: MediaType,
|
||||||
|
deleteOriginal: Boolean,
|
||||||
|
): Result<MediaUploadInfo> {
|
||||||
|
val result = mediaPreProcessor.process(uri, mediaType, deleteOriginal = deleteOriginal)
|
||||||
|
Timber.d("Pre-processed media result: $result")
|
||||||
|
return result.onFailure {
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(StringR.string.screen_media_upload_preview_error_failed_processing))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_room_attachment_source_camera">"Kamera"</string>
|
||||||
|
<string name="screen_room_attachment_source_camera_photo">"Foto aufnehmen"</string>
|
||||||
|
<string name="screen_room_attachment_source_camera_video">"Video aufnehmen"</string>
|
||||||
|
<string name="screen_room_attachment_source_files">"Anhang"</string>
|
||||||
|
<string name="screen_room_attachment_source_gallery">"Foto- & Video-Bibliothek"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages
|
package io.element.android.features.messages
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -31,14 +29,15 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediapickers.PickerProvider
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -132,8 +131,10 @@ class MessagesPresenterTest {
|
||||||
val messageComposerPresenter = MessageComposerPresenter(
|
val messageComposerPresenter = MessageComposerPresenter(
|
||||||
appCoroutineScope = this,
|
appCoroutineScope = this,
|
||||||
room = matrixRoom,
|
room = matrixRoom,
|
||||||
mediaPickerProvider = PickerProvider(isInTest = true),
|
mediaPickerProvider = FakePickerProvider(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
val timelinePresenter = TimelinePresenter(
|
val timelinePresenter = TimelinePresenter(
|
||||||
timelineItemsFactory = aTimelineItemsFactory(),
|
timelineItemsFactory = aTimelineItemsFactory(),
|
||||||
|
|
@ -146,6 +147,7 @@ class MessagesPresenterTest {
|
||||||
timelinePresenter = timelinePresenter,
|
timelinePresenter = timelinePresenter,
|
||||||
actionListPresenter = actionListPresenter,
|
actionListPresenter = actionListPresenter,
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.actionlist
|
package io.element.android.features.messages.actionlist
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -23,9 +21,9 @@ import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
import io.element.android.features.messages.impl.actionlist.ActionListEvents
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListState
|
import io.element.android.features.messages.impl.actionlist.ActionListState
|
||||||
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent
|
||||||
|
|
@ -37,7 +35,6 @@ import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.fixtures
|
package io.element.android.features.messages.fixtures
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
|
||||||
|
|
@ -34,7 +32,6 @@ import io.element.android.features.messages.impl.timeline.factories.virtual.Time
|
||||||
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
|
|
||||||
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
||||||
dispatchers = testCoroutineDispatchers(),
|
dispatchers = testCoroutineDispatchers(),
|
||||||
|
|
|
||||||
|
|
@ -22,45 +22,59 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.ReceiveTurbine
|
import app.cash.turbine.ReceiveTurbine
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
|
import io.element.android.features.messages.impl.textcomposer.AttachmentSourcePicker
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerEvents
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
|
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
|
||||||
import io.element.android.libraries.core.data.StableCharSequence
|
import io.element.android.libraries.core.data.StableCharSequence
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
|
import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.A_MESSAGE
|
import io.element.android.libraries.matrix.test.A_MESSAGE
|
||||||
import io.element.android.libraries.matrix.test.A_REPLY
|
import io.element.android.libraries.matrix.test.A_REPLY
|
||||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediapickers.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
|
import io.element.android.libraries.mediapickers.test.FakePickerProvider
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
||||||
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.textcomposer.MessageComposerMode
|
import io.element.android.libraries.textcomposer.MessageComposerMode
|
||||||
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.runCurrent
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class MessageComposerPresenterTest {
|
class MessageComposerPresenterTest {
|
||||||
|
|
||||||
private val pickerProvider = PickerProvider(isInTest = true)
|
private val pickerProvider = FakePickerProvider().apply {
|
||||||
|
givenResult(mockk()) // Uri is not available in JVM, so the only way to have a non-null Uri is using Mockk
|
||||||
|
}
|
||||||
private val featureFlagService = FakeFeatureFlagService().apply {
|
private val featureFlagService = FakeFeatureFlagService().apply {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true)
|
setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val mediaPreProcessor = FakeMediaPreProcessor()
|
||||||
|
private val snackbarDispatcher = SnackbarDispatcher()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -74,12 +88,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - toggle fullscreen`() = runTest {
|
fun `present - toggle fullscreen`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -95,12 +104,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change message`() = runTest {
|
fun `present - change message`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -118,12 +122,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change mode to edit`() = runTest {
|
fun `present - change mode to edit`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -139,23 +138,9 @@ class MessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
|
|
||||||
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
|
||||||
skipItems(skipCount)
|
|
||||||
val normalState = awaitItem()
|
|
||||||
assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
|
||||||
assertThat(normalState.text).isEqualTo(StableCharSequence(""))
|
|
||||||
assertThat(normalState.isSendButtonVisible).isFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change mode to reply`() = runTest {
|
fun `present - change mode to reply`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -172,12 +157,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change mode to quote`() = runTest {
|
fun `present - change mode to quote`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -194,12 +174,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send message`() = runTest {
|
fun `present - send message`() = runTest {
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this)
|
||||||
this,
|
|
||||||
FakeMatrixRoom(),
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -218,11 +193,9 @@ class MessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - edit message`() = runTest {
|
fun `present - edit message`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val fakeMatrixRoom = FakeMatrixRoom()
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
this,
|
||||||
fakeMatrixRoom,
|
fakeMatrixRoom,
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -251,11 +224,9 @@ class MessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - reply message`() = runTest {
|
fun `present - reply message`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val fakeMatrixRoom = FakeMatrixRoom()
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(
|
||||||
this,
|
this,
|
||||||
fakeMatrixRoom,
|
fakeMatrixRoom,
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -283,13 +254,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Open attachments menu`() = runTest {
|
fun `present - Open attachments menu`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val presenter = createPresenter(this)
|
||||||
val presenter = MessageComposerPresenter(
|
|
||||||
this,
|
|
||||||
fakeMatrixRoom,
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -302,13 +267,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Open camera attachments menu`() = runTest {
|
fun `present - Open camera attachments menu`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val presenter = createPresenter(this)
|
||||||
val presenter = MessageComposerPresenter(
|
|
||||||
this,
|
|
||||||
fakeMatrixRoom,
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -321,13 +280,7 @@ class MessageComposerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Dismiss attachments menu`() = runTest {
|
fun `present - Dismiss attachments menu`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val presenter = createPresenter(this)
|
||||||
val presenter = MessageComposerPresenter(
|
|
||||||
this,
|
|
||||||
fakeMatrixRoom,
|
|
||||||
pickerProvider,
|
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -341,80 +294,204 @@ class MessageComposerPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Pick media from gallery`() = runTest {
|
fun `present - Pick image from gallery`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this, room = room)
|
||||||
this,
|
pickerProvider.givenMimeType(MimeTypes.Images)
|
||||||
fakeMatrixRoom,
|
mediaPreProcessor.givenResult(
|
||||||
pickerProvider,
|
Result.success(
|
||||||
featureFlagService,
|
MediaUploadInfo.Image(
|
||||||
|
file = File("/some/path"),
|
||||||
|
info = ImageInfo(
|
||||||
|
width = null,
|
||||||
|
height = null,
|
||||||
|
mimetype = null,
|
||||||
|
size = null,
|
||||||
|
thumbnailInfo = null,
|
||||||
|
thumbnailUrl = null,
|
||||||
|
blurhash = null,
|
||||||
|
),
|
||||||
|
thumbnailInfo = ThumbnailProcessingInfo(
|
||||||
|
file = File("/some/path"),
|
||||||
|
info = ThumbnailInfo(
|
||||||
|
width = null,
|
||||||
|
height = null,
|
||||||
|
mimetype = null,
|
||||||
|
size = null,
|
||||||
|
),
|
||||||
|
blurhash = "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
|
// Wait for the launched upload coroutine to run
|
||||||
|
runCurrent()
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO verify some post processing of the selected media is done
|
@Test
|
||||||
|
fun `present - Pick video from gallery`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createPresenter(this, room = room)
|
||||||
|
pickerProvider.givenMimeType(MimeTypes.Videos)
|
||||||
|
mediaPreProcessor.givenResult(
|
||||||
|
Result.success(
|
||||||
|
MediaUploadInfo.Video(
|
||||||
|
file = File("/some/path"),
|
||||||
|
info = VideoInfo(
|
||||||
|
width = null,
|
||||||
|
height = null,
|
||||||
|
mimetype = null,
|
||||||
|
duration = null,
|
||||||
|
size = null,
|
||||||
|
thumbnailInfo = null,
|
||||||
|
thumbnailUrl = null,
|
||||||
|
blurhash = null,
|
||||||
|
),
|
||||||
|
thumbnailInfo = ThumbnailProcessingInfo(
|
||||||
|
file = File("/some/path"),
|
||||||
|
info = ThumbnailInfo(
|
||||||
|
width = null,
|
||||||
|
height = null,
|
||||||
|
mimetype = null,
|
||||||
|
size = null,
|
||||||
|
),
|
||||||
|
blurhash = "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
|
// Wait for the launched upload coroutine to run
|
||||||
|
runCurrent()
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Pick media from gallery fails if returned mimetype is not video or image`() = runTest {
|
||||||
|
val presenter = createPresenter(this)
|
||||||
|
pickerProvider.givenMimeType(MimeTypes.Audio)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
|
assertThat(awaitError()).isInstanceOf(IllegalStateException::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Pick media from gallery & cancel does nothing`() = runTest {
|
||||||
|
val presenter = createPresenter(this)
|
||||||
|
with(pickerProvider) {
|
||||||
|
givenResult(null) // Simulate a user canceling the flow
|
||||||
|
givenMimeType(MimeTypes.Images)
|
||||||
|
}
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery)
|
||||||
|
// No crashes here, otherwise it fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - Pick file from storage`() = runTest {
|
fun `present - Pick file from storage`() = runTest {
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
val presenter = MessageComposerPresenter(
|
val presenter = createPresenter(this, room = room)
|
||||||
this,
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
fakeMatrixRoom,
|
presenter.present()
|
||||||
pickerProvider,
|
}.test {
|
||||||
featureFlagService,
|
val initialState = awaitItem()
|
||||||
)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||||
|
// Wait for the launched upload coroutine to run
|
||||||
|
runCurrent()
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Take photo`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createPresenter(this, room = room)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo)
|
||||||
|
// Wait for the launched upload coroutine to run
|
||||||
|
runCurrent()
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Record video`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val presenter = createPresenter(this, room = room)
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video)
|
||||||
|
// Wait for the launched upload coroutine to run
|
||||||
|
runCurrent()
|
||||||
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - Uploading media failure can be recovered from`() = runTest {
|
||||||
|
val room = FakeMatrixRoom().apply {
|
||||||
|
givenSendMediaResult(Result.failure(Exception()))
|
||||||
|
}
|
||||||
|
val presenter = createPresenter(this, room = room)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||||
|
|
||||||
// TODO verify some post processing of the selected media is done
|
snackbarDispatcher.snackbarMessage.test {
|
||||||
|
// Initial value is always null
|
||||||
|
skipItems(1)
|
||||||
|
// Assert error message received
|
||||||
|
assertThat(awaitItem()).isNotNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private suspend fun ReceiveTurbine<MessageComposerState>.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) {
|
||||||
fun `present - Take photo`() = runTest {
|
state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode)
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
skipItems(skipCount)
|
||||||
val presenter = MessageComposerPresenter(
|
val normalState = awaitItem()
|
||||||
this,
|
assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal(""))
|
||||||
fakeMatrixRoom,
|
assertThat(normalState.text).isEqualTo(StableCharSequence(""))
|
||||||
pickerProvider,
|
assertThat(normalState.isSendButtonVisible).isFalse()
|
||||||
featureFlagService,
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Photo)
|
|
||||||
|
|
||||||
// TODO verify some post processing of the captured image is done
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private fun createPresenter(
|
||||||
fun `present - Record video`() = runTest {
|
coroutineScope: CoroutineScope,
|
||||||
val fakeMatrixRoom = FakeMatrixRoom()
|
room: MatrixRoom = FakeMatrixRoom(),
|
||||||
val presenter = MessageComposerPresenter(
|
pickerProvider: PickerProvider = this.pickerProvider,
|
||||||
this,
|
featureFlagService: FeatureFlagService = this.featureFlagService,
|
||||||
fakeMatrixRoom,
|
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||||
pickerProvider,
|
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||||
featureFlagService,
|
) = MessageComposerPresenter(
|
||||||
)
|
coroutineScope, room, pickerProvider, featureFlagService, mediaPreProcessor, snackbarDispatcher
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
)
|
||||||
presenter.present()
|
|
||||||
}.test {
|
|
||||||
val initialState = awaitItem()
|
|
||||||
initialState.eventSink(MessageComposerEvents.PickCameraAttachmentSource.Video)
|
|
||||||
|
|
||||||
// TODO verify some post processing of the captured video is done
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.messages.timeline
|
package io.element.android.features.messages.timeline
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -27,8 +25,6 @@ import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
import io.element.android.features.messages.impl.timeline.TimelinePresenter
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,11 @@
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import com.airbnb.android.showkase.ui.ShowkaseBrowserActivity
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
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
|
||||||
|
|
@ -35,11 +38,21 @@ class DeveloperSettingsNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
|
val activity = LocalContext.current as Activity
|
||||||
|
fun openShowkase() {
|
||||||
|
val intent = ShowkaseBrowserActivity.getIntent(
|
||||||
|
context = activity,
|
||||||
|
rootModuleCanonicalName = "io.element.android.libraries.designsystem.showkase.DesignSystemShowkaseRootModule"
|
||||||
|
)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
DeveloperSettingsView(
|
DeveloperSettingsView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackPressed = this::navigateUp
|
onOpenShowkase = ::openShowkase,
|
||||||
|
onBackPressed = ::navigateUp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,28 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.statusBars
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.Composable
|
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.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.libraries.designsystem.components.preferences.PreferenceTopAppBar
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceText
|
||||||
|
import io.element.android.libraries.designsystem.components.preferences.PreferenceView
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
|
||||||
import io.element.android.libraries.featureflag.ui.FeatureListView
|
import io.element.android.libraries.featureflag.ui.FeatureListView
|
||||||
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
|
||||||
import io.element.android.libraries.ui.strings.R
|
import io.element.android.libraries.ui.strings.R
|
||||||
|
|
@ -43,45 +33,42 @@ import io.element.android.libraries.ui.strings.R
|
||||||
@Composable
|
@Composable
|
||||||
fun DeveloperSettingsView(
|
fun DeveloperSettingsView(
|
||||||
state: DeveloperSettingsState,
|
state: DeveloperSettingsState,
|
||||||
modifier: Modifier = Modifier,
|
onOpenShowkase: () -> Unit,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
PreferenceView(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.fillMaxSize()
|
onBackPressed = onBackPressed,
|
||||||
.systemBarsPadding()
|
title = stringResource(id = R.string.common_developer_options)
|
||||||
.imePadding(),
|
) {
|
||||||
contentWindowInsets = WindowInsets.statusBars,
|
// Note: this is OK to hardcode strings in this debug screen.
|
||||||
topBar = {
|
PreferenceCategory(title = "Feature flags") {
|
||||||
PreferenceTopAppBar(
|
FeatureListContent(state)
|
||||||
title = stringResource(id = R.string.common_developer_options),
|
|
||||||
onBackPressed = onBackPressed,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
content = {
|
|
||||||
FeatureListContent(it, state)
|
|
||||||
}
|
}
|
||||||
)
|
PreferenceCategory(title = "Showkase") {
|
||||||
|
PreferenceText(
|
||||||
|
title = "Open Showkase browser",
|
||||||
|
onClick = onOpenShowkase
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeatureListContent(
|
fun FeatureListContent(
|
||||||
paddingValues: PaddingValues,
|
|
||||||
state: DeveloperSettingsState,
|
state: DeveloperSettingsState,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) {
|
fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) {
|
||||||
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled))
|
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
FeatureListView(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.padding(paddingValues)
|
features = state.features,
|
||||||
.fillMaxSize()
|
onCheckedChange = ::onFeatureEnabled,
|
||||||
) {
|
)
|
||||||
FeatureListView(features = state.features, onCheckedChange = ::onFeatureEnabled)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
|
@ -98,6 +85,7 @@ fun DeveloperSettingsViewDarkPreview(@PreviewParameter(DeveloperSettingsStatePro
|
||||||
private fun ContentToPreview(state: DeveloperSettingsState) {
|
private fun ContentToPreview(state: DeveloperSettingsState) {
|
||||||
DeveloperSettingsView(
|
DeveloperSettingsView(
|
||||||
state = state,
|
state = state,
|
||||||
|
onOpenShowkase = {},
|
||||||
onBackPressed = {}
|
onBackPressed = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.developer
|
package io.element.android.features.preferences.impl.developer
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -24,7 +22,6 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.preferences.impl.root
|
package io.element.android.features.preferences.impl.root
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -29,7 +27,6 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@
|
||||||
<string name="screen_bug_report_include_logs">"Trimiteți log-uri pentru a ajuta"</string>
|
<string name="screen_bug_report_include_logs">"Trimiteți log-uri pentru a ajuta"</string>
|
||||||
<string name="screen_bug_report_include_screenshot">"Trimiteți captură de ecran"</string>
|
<string name="screen_bug_report_include_screenshot">"Trimiteți captură de ecran"</string>
|
||||||
<string name="screen_bug_report_logs_description">"Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare."</string>
|
<string name="screen_bug_report_logs_description">"Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare."</string>
|
||||||
<string name="screen_bug_report_rash_logs_alert_title">"%1$s s-a blocat ultima dată când a fost folosit. Dorești să ne trimiti un raport?"</string>
|
<string name="screen_bug_report_rash_logs_alert_title">"%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.bugreport
|
package io.element.android.features.rageshake.impl.bugreport
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -28,7 +26,6 @@ import io.element.android.features.rageshake.test.screenshot.A_SCREENSHOT_URI
|
||||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.crash.ui
|
package io.element.android.features.rageshake.impl.crash.ui
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -26,7 +24,6 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionEvents
|
||||||
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
import io.element.android.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
||||||
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
import io.element.android.features.rageshake.test.crash.A_CRASH_DATA
|
||||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.detection
|
package io.element.android.features.rageshake.impl.detection
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
|
@ -31,10 +29,10 @@ import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataSto
|
||||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class RageshakeDetectionPresenterTest {
|
class RageshakeDetectionPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -101,7 +99,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test(timeout = 30.seconds) {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.isStarted).isFalse()
|
assertThat(initialState.isStarted).isFalse()
|
||||||
|
|
@ -155,7 +153,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - screenshot then disable`() = runTest {
|
fun `present - screenshot then disable`() = runTest(timeout = 1.seconds) {
|
||||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||||
val rageshake = FakeRageShake(isAvailableValue = true)
|
val rageshake = FakeRageShake(isAvailableValue = true)
|
||||||
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.preferences
|
package io.element.android.features.rageshake.impl.preferences
|
||||||
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
|
|
@ -26,7 +24,6 @@ import io.element.android.features.rageshake.api.preferences.RageshakePreference
|
||||||
import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY
|
import io.element.android.features.rageshake.test.rageshake.A_SENSITIVITY
|
||||||
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
|
||||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ dependencies {
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.features.userlist.api)
|
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
api(projects.features.roomdetails.api)
|
api(projects.features.roomdetails.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
@ -51,7 +50,6 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.features.userlist.impl)
|
|
||||||
testImplementation(projects.features.userlist.test)
|
testImplementation(projects.features.userlist.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -124,7 +125,7 @@ class RoomDetailsPresenter @Inject constructor(
|
||||||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||||
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
is MatrixRoomMembersState.Pending -> Async.Loading(prevState = membersState.prevRoomMembers?.size)
|
||||||
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
is MatrixRoomMembersState.Error -> Async.Failure(membersState.failure, prevState = membersState.prevRoomMembers?.size)
|
||||||
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.size)
|
is MatrixRoomMembersState.Ready -> Async.Success(membersState.roomMembers.count { it.membership == RoomMembershipState.JOIN })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,17 @@
|
||||||
package io.element.android.features.roomdetails.impl.di
|
package io.element.android.features.roomdetails.impl.di
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesTo
|
import com.squareup.anvil.annotations.ContributesTo
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource
|
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.di.RoomScope
|
import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@ContributesTo(RoomScope::class)
|
@ContributesTo(RoomScope::class)
|
||||||
interface RoomMemberBindsModule {
|
object RoomMemberModule {
|
||||||
|
|
||||||
@Binds
|
|
||||||
@Named("RoomMembers")
|
|
||||||
fun bindRoomMemberUserListDataSource(dataSource: RoomUserListDataSource): UserListDataSource
|
|
||||||
}
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@ContributesTo(RoomScope::class)
|
|
||||||
object RoomMemberProvidesModule {
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun provideRoomMemberDetailsPresenterFactory(
|
fun provideRoomMemberDetailsPresenterFactory(
|
||||||
|
|
@ -16,27 +16,23 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.room.roomMembers
|
import io.element.android.libraries.matrix.api.room.roomMembers
|
||||||
import io.element.android.libraries.matrix.api.room.toMatrixUser
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import kotlinx.coroutines.flow.dropWhile
|
import kotlinx.coroutines.flow.dropWhile
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomUserListDataSource @Inject constructor(
|
class RoomMemberListDataSource @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : UserListDataSource {
|
) {
|
||||||
|
|
||||||
override suspend fun search(query: String): List<MatrixUser> = withContext(coroutineDispatchers.io) {
|
suspend fun search(query: String): List<RoomMember> = withContext(coroutineDispatchers.io) {
|
||||||
val roomMembers = room.membersStateFlow
|
val roomMembers = room.membersStateFlow
|
||||||
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
||||||
.first()
|
.first()
|
||||||
|
|
@ -50,11 +46,7 @@ class RoomUserListDataSource @Inject constructor(
|
||||||
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
|
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filteredMembers.map(RoomMember::toMatrixUser)
|
filteredMembers
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
|
|
||||||
sealed interface RoomMemberListEvents {
|
sealed interface RoomMemberListEvents {
|
||||||
data class SelectUser(val user: MatrixUser) : RoomMemberListEvents
|
data class UpdateSearchQuery(val query: String) : RoomMemberListEvents
|
||||||
|
data class OnSearchActiveChanged(val active: Boolean) : RoomMemberListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,54 +18,73 @@ package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import androidx.compose.runtime.setValue
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
|
||||||
|
|
||||||
class RoomMemberListPresenter @Inject constructor(
|
class RoomMemberListPresenter @Inject constructor(
|
||||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||||
@Named("RoomMembers") private val userListDataSource: UserListDataSource,
|
|
||||||
private val userListDataStore: UserListDataStore,
|
|
||||||
private val room: MatrixRoom,
|
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<RoomMemberListState> {
|
) : Presenter<RoomMemberListState> {
|
||||||
|
|
||||||
private val userListPresenter by lazy {
|
|
||||||
userListPresenterFactory.create(
|
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
|
||||||
userListDataSource,
|
|
||||||
userListDataStore,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): RoomMemberListState {
|
override fun present(): RoomMemberListState {
|
||||||
val userListState = userListPresenter.present()
|
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
var searchResults by remember {
|
||||||
|
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
||||||
|
}
|
||||||
|
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
allUsers.value = Async.Success(userListDataSource.search("").toImmutableList())
|
val members = roomMemberListDataSource.search("").groupBy { it.membership }
|
||||||
|
roomMembers = Async.Success(
|
||||||
|
RoomMembers(
|
||||||
|
invited = members.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
|
joined = members.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
withContext(coroutineDispatchers.io) {
|
||||||
|
searchResults = if (searchQuery.isEmpty()) {
|
||||||
|
RoomMemberSearchResultState.NotSearching
|
||||||
|
} else {
|
||||||
|
val results = roomMemberListDataSource.search(searchQuery).groupBy { it.membership }
|
||||||
|
if (results.isEmpty()) RoomMemberSearchResultState.NoResults
|
||||||
|
else RoomMemberSearchResultState.Results(
|
||||||
|
RoomMembers(
|
||||||
|
invited = results.getOrDefault(RoomMembershipState.INVITE, emptyList()).toImmutableList(),
|
||||||
|
joined = results.getOrDefault(RoomMembershipState.JOIN, emptyList()).toImmutableList(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomMemberListState(
|
return RoomMemberListState(
|
||||||
allUsers = allUsers.value,
|
roomMembers = roomMembers,
|
||||||
userListState = userListState,
|
searchQuery = searchQuery,
|
||||||
|
searchResults = searchResults,
|
||||||
|
isSearchActive = isSearchActive,
|
||||||
|
eventSink = { event ->
|
||||||
|
when (event) {
|
||||||
|
is RoomMemberListEvents.OnSearchActiveChanged -> isSearchActive = event.active
|
||||||
|
is RoomMemberListEvents.UpdateSearchQuery -> searchQuery = event.query
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,30 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import io.element.android.features.userlist.api.UserListState
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class RoomMemberListState(
|
data class RoomMemberListState(
|
||||||
val allUsers: Async<ImmutableList<MatrixUser>>,
|
val roomMembers: Async<RoomMembers>,
|
||||||
val userListState: UserListState,
|
val searchQuery: String,
|
||||||
|
val searchResults: RoomMemberSearchResultState,
|
||||||
|
val isSearchActive: Boolean,
|
||||||
|
val eventSink: (RoomMemberListEvents) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class RoomMembers(
|
||||||
|
val invited: ImmutableList<RoomMember>,
|
||||||
|
val joined: ImmutableList<RoomMember>
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface RoomMemberSearchResultState {
|
||||||
|
/** No search results are available yet (e.g. because the user hasn't entered a (long enough) search term). */
|
||||||
|
object NotSearching : RoomMemberSearchResultState
|
||||||
|
|
||||||
|
/** The search has completed, but no results were found. */
|
||||||
|
object NoResults : RoomMemberSearchResultState
|
||||||
|
|
||||||
|
/** The search has completed, and some matching users were found. */
|
||||||
|
data class Results(val results: RoomMembers) : RoomMemberSearchResultState
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,93 @@
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.api.aUserListState
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||||
override val values: Sequence<RoomMemberListState>
|
override val values: Sequence<RoomMemberListState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))),
|
aRoomMemberListState(
|
||||||
aRoomMemberListState(allUsers = Async.Loading())
|
roomMembers = Async.Success(
|
||||||
|
RoomMembers(
|
||||||
|
invited = persistentListOf(aVictor(), aWalter()),
|
||||||
|
joined = persistentListOf(anAlice(), aBob()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aRoomMemberListState(roomMembers = Async.Loading()),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = false),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = true),
|
||||||
|
aRoomMemberListState().copy(isSearchActive = true, searchQuery = "someone"),
|
||||||
|
aRoomMemberListState().copy(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "@someone:matrix.org",
|
||||||
|
searchResults = RoomMemberSearchResultState.Results(
|
||||||
|
RoomMembers(
|
||||||
|
invited = persistentListOf(aVictor()),
|
||||||
|
joined = persistentListOf(anAlice()),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
aRoomMemberListState().copy(
|
||||||
|
isSearchActive = true,
|
||||||
|
searchQuery = "something-with-no-results",
|
||||||
|
searchResults = RoomMemberSearchResultState.NoResults
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aRoomMemberListState(
|
internal fun aRoomMemberListState(
|
||||||
searchResults: UserSearchResultState = UserSearchResultState.NotSearching,
|
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
||||||
) =
|
) = RoomMemberListState(
|
||||||
RoomMemberListState(
|
roomMembers = roomMembers,
|
||||||
userListState = aUserListState().copy(searchResults = searchResults),
|
searchQuery = "",
|
||||||
allUsers = allUsers,
|
searchResults = searchResults,
|
||||||
)
|
isSearchActive = false,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aRoomMember(
|
||||||
|
userId: UserId = UserId("@alice:server.org"),
|
||||||
|
displayName: String? = null,
|
||||||
|
avatarUrl: String? = null,
|
||||||
|
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
||||||
|
isNameAmbiguous: Boolean = false,
|
||||||
|
powerLevel: Long = 0L,
|
||||||
|
normalizedPowerLevel: Long = 0L,
|
||||||
|
isIgnored: Boolean = false,
|
||||||
|
) = RoomMember(
|
||||||
|
userId = userId,
|
||||||
|
displayName = displayName,
|
||||||
|
avatarUrl = avatarUrl,
|
||||||
|
membership = membership,
|
||||||
|
isNameAmbiguous = isNameAmbiguous,
|
||||||
|
powerLevel = powerLevel,
|
||||||
|
normalizedPowerLevel = normalizedPowerLevel,
|
||||||
|
isIgnored = isIgnored,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aRoomMemberList() = listOf(
|
||||||
|
anAlice(),
|
||||||
|
aBob(),
|
||||||
|
aRoomMember(UserId("@carol:server.org"), "Carol"),
|
||||||
|
aRoomMember(UserId("@david:server.org"), "David"),
|
||||||
|
aRoomMember(UserId("@eve:server.org"), "Eve"),
|
||||||
|
aRoomMember(UserId("@justin:server.org"), "Justin"),
|
||||||
|
aRoomMember(UserId("@mallory:server.org"), "Mallory"),
|
||||||
|
aRoomMember(UserId("@susie:server.org"), "Susie"),
|
||||||
|
aVictor(),
|
||||||
|
aWalter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun anAlice() = aRoomMember(UserId("@alice:server.org"), "Alice")
|
||||||
|
fun aBob() = aRoomMember(UserId("@bob:server.org"), "Bob")
|
||||||
|
|
||||||
|
fun aVictor() = aRoomMember(UserId("@victor:server.org"), "Victor", membership = RoomMembershipState.INVITE)
|
||||||
|
|
||||||
|
fun aWalter() = aRoomMember(UserId("@walter:server.org"), "Walter", membership = RoomMembershipState.INVITE)
|
||||||
|
|
|
||||||
|
|
@ -16,20 +16,31 @@
|
||||||
|
|
||||||
package io.element.android.features.roomdetails.impl.members
|
package io.element.android.features.roomdetails.impl.members
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
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.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -39,37 +50,43 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
import io.element.android.features.userlist.api.components.SearchSingleUserResultItem
|
|
||||||
import io.element.android.features.userlist.api.components.UserListView
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.architecture.isLoading
|
import io.element.android.libraries.architecture.isLoading
|
||||||
import io.element.android.libraries.designsystem.ElementTextStyles
|
import io.element.android.libraries.designsystem.ElementTextStyles
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
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.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
|
||||||
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.IconButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.SearchBar
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import io.element.android.libraries.matrix.ui.components.MatrixUserRow
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import io.element.android.libraries.ui.strings.R as StringR
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListView(
|
fun RoomMemberListView(
|
||||||
state: RoomMemberListState,
|
state: RoomMemberListState,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onMemberSelected: (UserId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
|
||||||
onMemberSelected: (UserId) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun onUserSelected(user: MatrixUser) {
|
fun onUserSelected(roomMember: RoomMember) {
|
||||||
onMemberSelected(user.userId)
|
onMemberSelected(roomMember.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (!state.userListState.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,33 +97,26 @@ fun RoomMemberListView(
|
||||||
.padding(padding),
|
.padding(padding),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
UserListView(
|
Column {
|
||||||
state = state.userListState,
|
RoomMemberSearchBar(
|
||||||
onUserSelected = ::onUserSelected,
|
query = state.searchQuery,
|
||||||
)
|
state = state.searchResults,
|
||||||
|
active = state.isSearchActive,
|
||||||
|
placeHolderTitle = stringResource(StringR.string.common_search_for_someone),
|
||||||
|
onActiveChanged = { state.eventSink(RoomMemberListEvents.OnSearchActiveChanged(it)) },
|
||||||
|
onTextChanged = { state.eventSink(RoomMemberListEvents.UpdateSearchQuery(it)) },
|
||||||
|
onUserSelected = ::onUserSelected,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.userListState.isSearchActive) {
|
if (!state.isSearchActive) {
|
||||||
if (state.allUsers is Async.Success) {
|
if (state.roomMembers is Async.Success) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
RoomMemberList(
|
||||||
item {
|
roomMembers = state.roomMembers.state,
|
||||||
val memberCount = state.allUsers.state.count()
|
onUserSelected = ::onUserSelected,
|
||||||
Text(
|
)
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
} else if (state.roomMembers.isLoading()) {
|
||||||
text = pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount),
|
|
||||||
style = ElementTextStyles.Regular.callout,
|
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
|
||||||
textAlign = TextAlign.Start,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
items(state.allUsers.state) { matrixUser ->
|
|
||||||
SearchSingleUserResultItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
matrixUser = matrixUser,
|
|
||||||
onClick = { onUserSelected(matrixUser) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (state.allUsers.isLoading()) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
|
|
@ -116,9 +126,73 @@ fun RoomMemberListView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberList(
|
||||||
|
roomMembers: RoomMembers,
|
||||||
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||||
|
if (roomMembers.invited.isNotEmpty()) {
|
||||||
|
roomMemberListSection(
|
||||||
|
headerText = { stringResource(id = R.string.screen_room_member_list_pending_header_title) },
|
||||||
|
members = roomMembers.invited,
|
||||||
|
onMemberSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (roomMembers.joined.isNotEmpty()) {
|
||||||
|
val memberCount = roomMembers.joined.count()
|
||||||
|
roomMemberListSection(
|
||||||
|
headerText = { pluralStringResource(id = R.plurals.screen_room_member_list_header_title, count = memberCount, memberCount) },
|
||||||
|
members = roomMembers.joined,
|
||||||
|
onMemberSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.roomMemberListSection(
|
||||||
|
headerText: @Composable () -> String,
|
||||||
|
members: ImmutableList<RoomMember>,
|
||||||
|
onMemberSelected: (RoomMember) -> Unit,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
text = headerText(),
|
||||||
|
style = ElementTextStyles.Regular.callout,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(members) { matrixUser ->
|
||||||
|
RoomMemberListItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
roomMember = matrixUser,
|
||||||
|
onClick = { onMemberSelected(matrixUser) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberListItem(
|
||||||
|
roomMember: RoomMember,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
MatrixUserRow(
|
||||||
|
modifier = modifier.clickable(onClick = onClick),
|
||||||
|
matrixUser = MatrixUser(
|
||||||
|
userId = roomMember.userId,
|
||||||
|
displayName = roomMember.displayName,
|
||||||
|
avatarUrl = roomMember.avatarUrl
|
||||||
|
),
|
||||||
|
avatarSize = AvatarSize.Custom(36.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListTopBar(
|
private fun RoomMemberListTopBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -135,6 +209,86 @@ fun RoomMemberListTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun RoomMemberSearchBar(
|
||||||
|
query: String,
|
||||||
|
state: RoomMemberSearchResultState,
|
||||||
|
active: Boolean,
|
||||||
|
placeHolderTitle: String,
|
||||||
|
onActiveChanged: (Boolean) -> Unit,
|
||||||
|
onTextChanged: (String) -> Unit,
|
||||||
|
onUserSelected: (RoomMember) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
onTextChanged("")
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchBar(
|
||||||
|
query = query,
|
||||||
|
onQueryChange = onTextChanged,
|
||||||
|
onSearch = { focusManager.clearFocus() },
|
||||||
|
active = active,
|
||||||
|
onActiveChange = onActiveChanged,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = if (!active) 16.dp else 0.dp),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = placeHolderTitle,
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = if (active) {
|
||||||
|
{ BackButton(onClick = { onActiveChanged(false) }) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
trailingIcon = when {
|
||||||
|
active && query.isNotEmpty() -> {
|
||||||
|
{
|
||||||
|
IconButton(onClick = { onTextChanged("") }) {
|
||||||
|
Icon(Icons.Default.Close, stringResource(StringR.string.action_clear))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!active -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = stringResource(StringR.string.action_search),
|
||||||
|
modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
content = {
|
||||||
|
if (state is RoomMemberSearchResultState.Results) {
|
||||||
|
RoomMemberList(
|
||||||
|
roomMembers = state.results,
|
||||||
|
onUserSelected = { onUserSelected(it) }
|
||||||
|
)
|
||||||
|
} else if (state is RoomMemberSearchResultState.NoResults) {
|
||||||
|
Spacer(Modifier.size(80.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(StringR.string.common_no_results),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||||
|
|
@ -147,5 +301,9 @@ fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::cla
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(state: RoomMemberListState) {
|
private fun ContentToPreview(state: RoomMemberListState) {
|
||||||
RoomMemberListView(state)
|
RoomMemberListView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = {},
|
||||||
|
onMemberSelected = {}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"Los mensajes están protegidos con \"candados\". Sólo tú y los destinatarios tenéis las llaves únicas para abrirlos."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Cifrado de mensajes activado"</string>
|
||||||
<string name="screen_room_details_invite_people_title">"Invitar a otras personas"</string>
|
|
||||||
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
<string name="screen_room_details_share_room_title">"Compartir sala"</string>
|
||||||
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
|
<string name="screen_dm_details_block_alert_action">"Bloquear"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
<string name="screen_dm_details_block_alert_description">"Los usuarios bloqueados no podrán enviarte mensajes y se ocultarán todos sus mensajes. Puede revertir esta acción en cualquier momento."</string>
|
||||||
|
|
@ -14,6 +13,7 @@
|
||||||
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
|
<string name="screen_dm_details_unblock_alert_action">"Desbloquear"</string>
|
||||||
<string name="screen_dm_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
|
<string name="screen_dm_details_unblock_alert_description">"Al desbloquear al usuario, podrás volver a ver todos sus mensajes."</string>
|
||||||
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
|
<string name="screen_dm_details_unblock_user">"Desbloquear usuario"</string>
|
||||||
|
<string name="screen_room_details_invite_people_title">"Invitar gente"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Salir de la sala"</string>
|
<string name="screen_room_details_leave_room_title">"Salir de la sala"</string>
|
||||||
<string name="screen_room_details_people_title">"Personas"</string>
|
<string name="screen_room_details_people_title">"Personas"</string>
|
||||||
<string name="screen_room_details_security_title">"Seguridad"</string>
|
<string name="screen_room_details_security_title">"Seguridad"</string>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"I messaggi sono protetti da lucchetti. Solo tu e i destinatari avete le chiavi univoche per sbloccarli."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Crittografia messaggi abilitata"</string>
|
||||||
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
|
||||||
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
<string name="screen_room_details_share_room_title">"Condividi stanza"</string>
|
||||||
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
|
<string name="screen_dm_details_block_alert_action">"Blocca"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
<string name="screen_dm_details_block_alert_description">"Gli utenti bloccati non saranno in grado di inviarti nuovi messaggi e tutti quelli già esistenti saranno nascosti. Potrai annullare questa azione in qualsiasi momento."</string>
|
||||||
|
|
@ -14,6 +13,7 @@
|
||||||
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
|
<string name="screen_dm_details_unblock_alert_action">"Sblocca"</string>
|
||||||
<string name="screen_dm_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
|
<string name="screen_dm_details_unblock_alert_description">"Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi."</string>
|
||||||
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
|
<string name="screen_dm_details_unblock_user">"Sblocca utente"</string>
|
||||||
|
<string name="screen_room_details_invite_people_title">"Invita persone"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Esci dalla stanza"</string>
|
<string name="screen_room_details_leave_room_title">"Esci dalla stanza"</string>
|
||||||
<string name="screen_room_details_people_title">"Persone"</string>
|
<string name="screen_room_details_people_title">"Persone"</string>
|
||||||
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Criptarea mesajelor este activată"</string>
|
||||||
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
|
||||||
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
<string name="screen_room_details_share_room_title">"Partajați camera"</string>
|
||||||
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
|
<string name="screen_dm_details_block_alert_action">"Blocați"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
<string name="screen_dm_details_block_alert_description">"Utilizatorii blocați nu vă vor putea trimite mesaje și toate mesajele lor vor fi ascunse. Puteți anula această acțiune oricând."</string>
|
||||||
|
|
@ -14,6 +13,7 @@
|
||||||
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
|
<string name="screen_dm_details_unblock_alert_action">"Deblocați"</string>
|
||||||
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
<string name="screen_dm_details_unblock_alert_description">"La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta."</string>
|
||||||
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
|
<string name="screen_dm_details_unblock_user">"Deblocați utilizatorul"</string>
|
||||||
|
<string name="screen_room_details_invite_people_title">"Invitați persoane"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Părăsiți camera"</string>
|
<string name="screen_room_details_leave_room_title">"Părăsiți camera"</string>
|
||||||
<string name="screen_room_details_people_title">"Persoane"</string>
|
<string name="screen_room_details_people_title">"Persoane"</string>
|
||||||
<string name="screen_room_details_security_title">"Securitate"</string>
|
<string name="screen_room_details_security_title">"Securitate"</string>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
|
||||||
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
|
||||||
<string name="screen_room_details_share_room_title">"Share room"</string>
|
<string name="screen_room_details_share_room_title">"Share room"</string>
|
||||||
|
<string name="screen_room_member_list_pending_header_title">"Pending"</string>
|
||||||
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
<string name="screen_dm_details_block_alert_action">"Block"</string>
|
||||||
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
<string name="screen_dm_details_block_alert_description">"Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime."</string>
|
||||||
<string name="screen_dm_details_block_user">"Block user"</string>
|
<string name="screen_dm_details_block_user">"Block user"</string>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import io.element.android.features.roomdetails.impl.LeaveRoomWarning
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
import io.element.android.features.roomdetails.impl.RoomDetailsEvent
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
import io.element.android.features.roomdetails.impl.RoomDetailsPresenter
|
||||||
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
import io.element.android.features.roomdetails.impl.RoomDetailsType
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -90,7 +92,7 @@ class RoomDetailsPresenterTests {
|
||||||
val room = aMatrixRoom()
|
val room = aMatrixRoom()
|
||||||
val roomMembers = listOf(
|
val roomMembers = listOf(
|
||||||
aRoomMember(A_USER_ID),
|
aRoomMember(A_USER_ID),
|
||||||
aRoomMember(A_USER_ID_2),
|
aRoomMember(A_USER_ID_2, membership = RoomMembershipState.INVITE),
|
||||||
)
|
)
|
||||||
val presenter = aRoomDetailsPresenter(room)
|
val presenter = aRoomDetailsPresenter(room)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
|
@ -112,7 +114,7 @@ class RoomDetailsPresenterTests {
|
||||||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||||
//skipItems(1)
|
//skipItems(1)
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(roomMembers.size))
|
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1))
|
||||||
|
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
|
|
@ -266,22 +268,3 @@ fun aMatrixRoom(
|
||||||
isDirect = isDirect,
|
isDirect = isDirect,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun aRoomMember(
|
|
||||||
userId: UserId = A_USER_ID,
|
|
||||||
displayName: String? = null,
|
|
||||||
avatarUrl: String? = null,
|
|
||||||
membership: RoomMembershipState = RoomMembershipState.JOIN,
|
|
||||||
isNameAmbiguous: Boolean = false,
|
|
||||||
powerLevel: Long = 0L,
|
|
||||||
normalizedPowerLevel: Long = 0L,
|
|
||||||
isIgnored: Boolean = false,
|
|
||||||
) = RoomMember(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
membership = membership,
|
|
||||||
isNameAmbiguous = isNameAmbiguous,
|
|
||||||
powerLevel = powerLevel,
|
|
||||||
normalizedPowerLevel = normalizedPowerLevel,
|
|
||||||
isIgnored = isIgnored,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -20,62 +20,111 @@ import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource
|
||||||
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter
|
||||||
import io.element.android.features.userlist.api.SelectionMode
|
import io.element.android.features.roomdetails.impl.members.RoomMemberSearchResultState
|
||||||
import io.element.android.features.userlist.api.UserListDataSource
|
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||||
import io.element.android.features.userlist.api.UserListDataStore
|
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||||
import io.element.android.features.userlist.api.UserListPresenter
|
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||||
import io.element.android.features.userlist.api.UserListPresenterArgs
|
|
||||||
import io.element.android.features.userlist.api.UserSearchResultState
|
|
||||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
|
||||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.libraries.architecture.Async
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.internal.toImmutableList
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
class RoomMemberListPresenterTests {
|
class RoomMemberListPresenterTests {
|
||||||
|
|
||||||
private val testCoroutineDispatchers = testCoroutineDispatchers()
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
fun `search is done automatically on start, but is async`() = runTest {
|
||||||
val searchResult = listOf(aMatrixUser())
|
val presenter = createPresenter()
|
||||||
val userListDataSource = FakeUserListDataSource().apply {
|
|
||||||
givenSearchResult(searchResult)
|
|
||||||
}
|
|
||||||
val userListDataStore = UserListDataStore()
|
|
||||||
val userListFactory = object : UserListPresenter.Factory {
|
|
||||||
override fun create(
|
|
||||||
args: UserListPresenterArgs,
|
|
||||||
userListDataSource: UserListDataSource,
|
|
||||||
userListDataStore: UserListDataStore,
|
|
||||||
) = DefaultUserListPresenter(args, userListDataSource, userListDataStore)
|
|
||||||
}
|
|
||||||
val fakeRoom = FakeMatrixRoom()
|
|
||||||
val presenter = RoomMemberListPresenter(
|
|
||||||
userListPresenterFactory = userListFactory,
|
|
||||||
userListDataSource = userListDataSource,
|
|
||||||
userListDataStore = userListDataStore,
|
|
||||||
room = fakeRoom,
|
|
||||||
coroutineDispatchers = testCoroutineDispatchers
|
|
||||||
)
|
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
Truth.assertThat(initialState.searchQuery).isEmpty()
|
||||||
Truth.assertThat(initialState.userListState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
Truth.assertThat(initialState.searchResults).isEqualTo(RoomMemberSearchResultState.NotSearching)
|
||||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
Truth.assertThat(initialState.isSearchActive).isFalse()
|
||||||
|
|
||||||
val loadedState = awaitItem()
|
val loadedState = awaitItem()
|
||||||
Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList())
|
Truth.assertThat(loadedState.roomMembers).isInstanceOf(Async.Success::class.java)
|
||||||
|
Truth.assertThat((loadedState.roomMembers as Async.Success).state.invited).isEqualTo(listOf(aVictor(), aWalter()))
|
||||||
|
Truth.assertThat((loadedState.roomMembers as Async.Success).state.joined).isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `open search`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
Truth.assertThat((searchActiveState.isSearchActive)).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search for something which is not found`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("something"))
|
||||||
|
val searchQueryUpdatedState = awaitItem()
|
||||||
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("something")
|
||||||
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.NoResults::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `search for something which is found`() = runTest {
|
||||||
|
val presenter = createPresenter()
|
||||||
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
val loadedState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.OnSearchActiveChanged(true))
|
||||||
|
val searchActiveState = awaitItem()
|
||||||
|
loadedState.eventSink(RoomMemberListEvents.UpdateSearchQuery("Alice"))
|
||||||
|
val searchQueryUpdatedState = awaitItem()
|
||||||
|
Truth.assertThat((searchQueryUpdatedState.searchQuery)).isEqualTo("Alice")
|
||||||
|
val searchSearchResultDelivered = awaitItem()
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults)).isInstanceOf(RoomMemberSearchResultState.Results::class.java)
|
||||||
|
Truth.assertThat((searchSearchResultDelivered.searchResults as RoomMemberSearchResultState.Results).results.joined.first().displayName)
|
||||||
|
.isEqualTo("Alice")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
private fun createDataSource(
|
||||||
|
matrixRoom: MatrixRoom = aMatrixRoom().apply {
|
||||||
|
givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList()))
|
||||||
|
},
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers)
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
private fun createPresenter(
|
||||||
|
roomMemberListDataSource: RoomMemberListDataSource = createDataSource(),
|
||||||
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers()
|
||||||
|
) = RoomMemberListPresenter(roomMemberListDataSource, coroutineDispatchers)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import io.element.android.features.roomdetails.aMatrixClient
|
import io.element.android.features.roomdetails.aMatrixClient
|
||||||
import io.element.android.features.roomdetails.aMatrixRoom
|
import io.element.android.features.roomdetails.aMatrixRoom
|
||||||
import io.element.android.features.roomdetails.aRoomMember
|
import io.element.android.features.roomdetails.impl.members.aRoomMember
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
|
||||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,9 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
internal class DefaultInviteStateDataSourceTest {
|
internal class DefaultInviteStateDataSourceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -133,5 +131,4 @@ internal class DefaultInviteStateDataSourceTest {
|
||||||
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,11 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class) class RoomListPresenterTests {
|
class RoomListPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - should start with no user and then load user with success`() = runTest {
|
fun `present - should start with no user and then load user with success`() = runTest {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.userlist.impl
|
package io.element.android.features.userlist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import app.cash.molecule.RecompositionClock
|
import app.cash.molecule.RecompositionClock
|
||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
|
|
@ -30,14 +29,10 @@ import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||||
import io.mockk.coJustRun
|
|
||||||
import io.mockk.mockkConstructor
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class DefaultUserListPresenterTests {
|
class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
private val userListDataSource = FakeUserListDataSource()
|
private val userListDataSource = FakeUserListDataSource()
|
||||||
|
|
@ -136,9 +131,6 @@ class DefaultUserListPresenterTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - select a user`() = runTest {
|
fun `present - select a user`() = runTest {
|
||||||
mockkConstructor(LazyListState::class)
|
|
||||||
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) }
|
|
||||||
|
|
||||||
val presenter = DefaultUserListPresenter(
|
val presenter = DefaultUserListPresenter(
|
||||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||||
userListDataSource,
|
userListDataSource,
|
||||||
|
|
@ -158,16 +150,15 @@ class DefaultUserListPresenterTests {
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.AddToSelection(userB))
|
initialState.eventSink(UserListEvents.AddToSelection(userB))
|
||||||
// the last added user should be presented first
|
assertThat(awaitItem().selectedUsers).containsExactly(userA, userB)
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userB, userA)
|
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.AddToSelection(userABis))
|
initialState.eventSink(UserListEvents.AddToSelection(userABis))
|
||||||
initialState.eventSink(UserListEvents.AddToSelection(userC))
|
initialState.eventSink(UserListEvents.AddToSelection(userC))
|
||||||
// duplicated users should be ignored
|
// duplicated users should be ignored
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userA, userB, userC)
|
||||||
|
|
||||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userB))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userB))
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA)
|
assertThat(awaitItem().selectedUsers).containsExactly(userA, userC)
|
||||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userA))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userA))
|
||||||
assertThat(awaitItem().selectedUsers).containsExactly(userC)
|
assertThat(awaitItem().selectedUsers).containsExactly(userC)
|
||||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userC))
|
initialState.eventSink(UserListEvents.RemoveFromSelection(userC))
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ signing.element.nightly.keyPassword=Secret
|
||||||
|
|
||||||
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
# Customise the Lint version to use a more recent version than the one bundled with AGP
|
||||||
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
# https://googlesamples.github.io/android-custom-lint-rules/usage/newer-lint.md.html
|
||||||
android.experimental.lint.version=8.0.0
|
android.experimental.lint.version=8.2.0-alpha02
|
||||||
|
|
||||||
# Enable test fixture for all modules by default
|
# Enable test fixture for all modules by default
|
||||||
android.experimental.enableTestFixtures=true
|
android.experimental.enableTestFixtures=true
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ molecule = "0.9.0"
|
||||||
|
|
||||||
# AndroidX
|
# AndroidX
|
||||||
material = "1.9.0"
|
material = "1.9.0"
|
||||||
core = "1.10.0"
|
core = "1.10.1"
|
||||||
datastore = "1.0.0"
|
datastore = "1.0.0"
|
||||||
constraintlayout = "2.1.4"
|
constraintlayout = "2.1.4"
|
||||||
recyclerview = "1.3.0"
|
recyclerview = "1.3.0"
|
||||||
|
|
@ -24,7 +24,7 @@ compose_bom = "2023.04.01"
|
||||||
composecompiler = "1.4.7"
|
composecompiler = "1.4.7"
|
||||||
|
|
||||||
# Coroutines
|
# Coroutines
|
||||||
coroutines = "1.6.4"
|
coroutines = "1.7.1"
|
||||||
|
|
||||||
# Accompanist
|
# Accompanist
|
||||||
accompanist = "0.30.1"
|
accompanist = "0.30.1"
|
||||||
|
|
@ -44,7 +44,7 @@ stem = "2.3.0"
|
||||||
sqldelight = "1.5.5"
|
sqldelight = "1.5.5"
|
||||||
|
|
||||||
# DI
|
# DI
|
||||||
dagger = "2.46"
|
dagger = "2.46.1"
|
||||||
anvil = "2.4.5"
|
anvil = "2.4.5"
|
||||||
|
|
||||||
# quality
|
# quality
|
||||||
|
|
@ -64,6 +64,7 @@ androidx_core = { module = "androidx.core:core", version.ref = "core" }
|
||||||
androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
androidx_corektx = { module = "androidx.core:core-ktx", version.ref = "core" }
|
||||||
androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
androidx_datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
|
||||||
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
androidx_datastore_datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||||
|
androidx_exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
|
||||||
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
androidx_constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
|
||||||
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
androidx_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||||
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||||
|
|
@ -117,7 +118,7 @@ test_orchestrator = "androidx.test:orchestrator:1.4.2"
|
||||||
test_turbine = "app.cash.turbine:turbine:0.12.3"
|
test_turbine = "app.cash.turbine:turbine:0.12.3"
|
||||||
test_truth = "com.google.truth:truth:1.1.3"
|
test_truth = "com.google.truth:truth:1.1.3"
|
||||||
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11"
|
test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.11"
|
||||||
test_robolectric = "org.robolectric:robolectric:4.10.1"
|
test_robolectric = "org.robolectric:robolectric:4.10.2"
|
||||||
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" }
|
||||||
|
|
||||||
# Others
|
# Others
|
||||||
|
|
@ -131,7 +132,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.11"
|
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.13"
|
||||||
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
|
||||||
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
|
||||||
|
|
@ -139,6 +140,8 @@ sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.4"
|
||||||
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
sqlite = "androidx.sqlite:sqlite:2.3.1"
|
||||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||||
gujun_span = "me.gujun.android:span:1.7"
|
gujun_span = "me.gujun.android:span:1.7"
|
||||||
|
otaliastudios_transcoder = "com.otaliastudios:transcoder:0.10.5"
|
||||||
|
vanniktech_blurhash = "com.vanniktech:blurhash:0.1.0"
|
||||||
|
|
||||||
# Di
|
# Di
|
||||||
inject = "javax.inject:javax.inject:1"
|
inject = "javax.inject:javax.inject:1"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ dependencies {
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.activity.activity)
|
implementation(libs.androidx.activity.activity)
|
||||||
|
implementation(libs.androidx.exifinterface)
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,13 @@
|
||||||
package io.element.android.libraries.androidutils.bitmap
|
package io.element.android.libraries.androidutils.bitmap
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
|
fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
|
||||||
outputStream().use { out ->
|
outputStream().use { out ->
|
||||||
|
|
@ -25,3 +31,71 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int
|
||||||
out.flush()
|
out.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the EXIF metadata from the [inputStream] and rotates the current [Bitmap] to match it.
|
||||||
|
* @return The resulting [Bitmap] or `null` if no metadata was found.
|
||||||
|
*/
|
||||||
|
fun Bitmap.rotateToMetadataOrientation(inputStream: InputStream): Result<Bitmap> =
|
||||||
|
runCatching { rotateToMetadataOrientation(this, ExifInterface(inputStream)) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales the current [Bitmap] to fit the ([maxWidth], [maxHeight]) bounds while keeping aspect ratio.
|
||||||
|
* @throws IllegalStateException if [maxWidth] or [maxHeight] <= 0.
|
||||||
|
*/
|
||||||
|
fun Bitmap.resizeToMax(maxWidth: Int, maxHeight: Int): Bitmap {
|
||||||
|
// No need to resize
|
||||||
|
if (this.width == maxWidth && this.height == maxHeight) return this
|
||||||
|
|
||||||
|
val aspectRatio = this.width.toFloat() / this.height.toFloat()
|
||||||
|
val useWidth = aspectRatio >= 1
|
||||||
|
val calculatedMaxWidth = min(this.width, maxWidth)
|
||||||
|
val calculatedMinHeight = min(this.height, maxHeight)
|
||||||
|
val width = if (useWidth) calculatedMaxWidth else (calculatedMinHeight * aspectRatio).toInt()
|
||||||
|
val height = if (useWidth) (calculatedMaxWidth / aspectRatio).toInt() else calculatedMinHeight
|
||||||
|
return scale(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates and returns [BitmapFactory.Options.inSampleSize] given a pair of [desiredWidth] & [desiredHeight]
|
||||||
|
* and the previously read [BitmapFactory.Options.outWidth] & [BitmapFactory.Options.outHeight].
|
||||||
|
*/
|
||||||
|
fun BitmapFactory.Options.calculateInSampleSize(desiredWidth: Int, desiredHeight: Int): Int {
|
||||||
|
var inSampleSize = 1
|
||||||
|
|
||||||
|
if (outWidth > desiredWidth || outHeight > desiredHeight) {
|
||||||
|
val halfHeight: Int = outHeight / 2
|
||||||
|
val halfWidth: Int = outWidth / 2
|
||||||
|
|
||||||
|
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||||
|
// height and width larger than the requested height and width.
|
||||||
|
while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) {
|
||||||
|
inSampleSize *= 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inSampleSize
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rotateToMetadataOrientation(bitmap: Bitmap, exifInterface: ExifInterface): Bitmap {
|
||||||
|
val orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
||||||
|
val matrix = Matrix()
|
||||||
|
when (orientation) {
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
||||||
|
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
|
||||||
|
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
|
||||||
|
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
||||||
|
matrix.preRotate(-90f)
|
||||||
|
matrix.preScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
||||||
|
matrix.preRotate(90f)
|
||||||
|
matrix.preScale(-1f, 1f)
|
||||||
|
}
|
||||||
|
else -> return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,13 @@
|
||||||
|
|
||||||
package io.element.android.libraries.androidutils.file
|
package io.element.android.libraries.androidutils.file
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
fun File.safeDelete() {
|
fun File.safeDelete() {
|
||||||
tryOrNull(
|
tryOrNull(
|
||||||
|
|
@ -32,3 +36,8 @@ fun File.safeDelete() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File = withContext(Dispatchers.IO) {
|
||||||
|
val suffix = extension?.let { ".$extension" }
|
||||||
|
File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.media
|
||||||
|
|
||||||
|
import android.media.MediaMetadataRetriever
|
||||||
|
|
||||||
|
/** [MediaMetadataRetriever] only implements `AutoClosable` since API 29, so we need to execute this to have the same in older APIs. */
|
||||||
|
inline fun <T> MediaMetadataRetriever.runAndRelease(block: MediaMetadataRetriever.() -> T): T {
|
||||||
|
return block().also { release() }
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,11 @@ object MimeTypes {
|
||||||
const val Jpeg = "image/jpeg"
|
const val Jpeg = "image/jpeg"
|
||||||
const val Gif = "image/gif"
|
const val Gif = "image/gif"
|
||||||
|
|
||||||
|
const val Videos = "video/*"
|
||||||
|
const val Mp4 = "video/mp4"
|
||||||
|
|
||||||
|
const val Audio = "audio/*"
|
||||||
|
|
||||||
const val Ogg = "audio/ogg"
|
const val Ogg = "audio/ogg"
|
||||||
|
|
||||||
const val PlainText = "text/plain"
|
const val PlainText = "text/plain"
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.featureflag.ui
|
package io.element.android.libraries.featureflag.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -34,14 +33,10 @@ fun FeatureListView(
|
||||||
onCheckedChange: (FeatureUiModel, Boolean) -> Unit,
|
onCheckedChange: (FeatureUiModel, Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
items(
|
features.forEach { feature ->
|
||||||
items = features,
|
|
||||||
key = { it.key }
|
|
||||||
) { feature ->
|
|
||||||
|
|
||||||
fun onCheckedChange(isChecked: Boolean) {
|
fun onCheckedChange(isChecked: Boolean) {
|
||||||
onCheckedChange(feature, isChecked)
|
onCheckedChange(feature, isChecked)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,6 @@ package io.element.android.libraries.matrix.api.media
|
||||||
|
|
||||||
data class AudioInfo(
|
data class AudioInfo(
|
||||||
val duration: Long?,
|
val duration: Long?,
|
||||||
val size: Long?
|
val size: Long?,
|
||||||
|
val mimeType: String?,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
|
||||||
interface NotificationService {
|
interface NotificationService {
|
||||||
suspend fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
fun getNotification(userId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,15 @@ import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
interface MatrixRoom : Closeable {
|
interface MatrixRoom : Closeable {
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
|
|
@ -67,6 +72,14 @@ interface MatrixRoom : Closeable {
|
||||||
|
|
||||||
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun sendFile(file: File, fileInfo: FileInfo): Result<Unit>
|
||||||
|
|
||||||
suspend fun leave(): Result<Unit>
|
suspend fun leave(): Result<Unit>
|
||||||
|
|
||||||
suspend fun acceptInvitation(): Result<Unit>
|
suspend fun acceptInvitation(): Result<Unit>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
package io.element.android.libraries.matrix.api.room
|
package io.element.android.libraries.matrix.api.room
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
|
||||||
|
|
||||||
data class RoomMember(
|
data class RoomMember(
|
||||||
val userId: UserId,
|
val userId: UserId,
|
||||||
|
|
@ -30,12 +29,6 @@ data class RoomMember(
|
||||||
val isIgnored: Boolean,
|
val isIgnored: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun RoomMember.toMatrixUser() = MatrixUser(
|
|
||||||
userId = userId,
|
|
||||||
displayName = displayName,
|
|
||||||
avatarUrl = avatarUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
enum class RoomMembershipState {
|
enum class RoomMembershipState {
|
||||||
BAN, INVITE, JOIN, KNOCK, LEAVE
|
BAN, INVITE, JOIN, KNOCK, LEAVE
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl
|
package io.element.android.libraries.matrix.impl
|
||||||
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
|
@ -44,7 +46,9 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -54,7 +58,9 @@ import kotlinx.coroutines.withTimeout
|
||||||
import org.matrix.rustcomponents.sdk.Client
|
import org.matrix.rustcomponents.sdk.Client
|
||||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||||
import org.matrix.rustcomponents.sdk.RequiredState
|
import org.matrix.rustcomponents.sdk.RequiredState
|
||||||
|
import org.matrix.rustcomponents.sdk.SlidingSyncList
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
|
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
|
||||||
|
import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncMode
|
import org.matrix.rustcomponents.sdk.SlidingSyncMode
|
||||||
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
|
|
@ -81,7 +87,7 @@ class RustMatrixClient constructor(
|
||||||
client = client,
|
client = client,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
private val notificationService = RustNotificationService(baseDirectory, dispatchers)
|
private val notificationService = RustNotificationService(client)
|
||||||
private var slidingSyncUpdateJob: Job? = null
|
private var slidingSyncUpdateJob: Job? = null
|
||||||
|
|
||||||
private val clientDelegate = object : ClientDelegate {
|
private val clientDelegate = object : ClientDelegate {
|
||||||
|
|
@ -104,7 +110,8 @@ class RustMatrixClient constructor(
|
||||||
notTags = emptyList()
|
notTags = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
|
private val visibleRoomsSlidingSyncList = MutableSharedFlow<SlidingSyncList>(replay = 1)
|
||||||
|
private val visibleRoomsSlidingSyncListBuilder = SlidingSyncListBuilder("CurrentlyVisibleRooms")
|
||||||
.timelineLimit(limit = 1u)
|
.timelineLimit(limit = 1u)
|
||||||
.requiredState(
|
.requiredState(
|
||||||
requiredState = listOf(
|
requiredState = listOf(
|
||||||
|
|
@ -114,16 +121,19 @@ class RustMatrixClient constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filters(visibleRoomsSlidingSyncFilters)
|
.filters(visibleRoomsSlidingSyncFilters)
|
||||||
.name(name = "CurrentlyVisibleRooms")
|
|
||||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||||
.addRange(0u, 20u)
|
.addRange(0u, 20u)
|
||||||
.use {
|
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||||
it.build()
|
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||||
}
|
visibleRoomsSlidingSyncList.tryEmit(list)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
private val invitesSlidingSyncFilters = visibleRoomsSlidingSyncFilters.copy(isInvite = true)
|
private val invitesSlidingSyncFilters = visibleRoomsSlidingSyncFilters.copy(isInvite = true)
|
||||||
|
|
||||||
private val invitesSlidingSyncList = SlidingSyncListBuilder()
|
private val invitesSlidingSyncList = MutableSharedFlow<SlidingSyncList>(replay = 1)
|
||||||
|
private val invitesSlidingSyncListBuilder = SlidingSyncListBuilder("CurrentInvites")
|
||||||
.timelineLimit(limit = 1u)
|
.timelineLimit(limit = 1u)
|
||||||
.requiredState(
|
.requiredState(
|
||||||
requiredState = listOf(
|
requiredState = listOf(
|
||||||
|
|
@ -133,20 +143,22 @@ class RustMatrixClient constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filters(invitesSlidingSyncFilters)
|
.filters(invitesSlidingSyncFilters)
|
||||||
.name(name = "CurrentInvites")
|
|
||||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||||
.addRange(0u, 20u)
|
.addRange(0u, 20u)
|
||||||
.use {
|
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||||
it.build()
|
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||||
}
|
invitesSlidingSyncList.tryEmit(list)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
private val slidingSync = client
|
private val slidingSync = client
|
||||||
.slidingSync()
|
.slidingSync()
|
||||||
.homeserver("https://slidingsync.lab.matrix.org")
|
.homeserver("https://slidingsync.lab.matrix.org")
|
||||||
.withCommonExtensions()
|
.withCommonExtensions()
|
||||||
.storageKey("ElementX")
|
.storageKey("ElementX")
|
||||||
.addList(visibleRoomsSlidingSyncList)
|
.addList(visibleRoomsSlidingSyncListBuilder)
|
||||||
.addList(invitesSlidingSyncList)
|
.addList(invitesSlidingSyncListBuilder)
|
||||||
.use {
|
.use {
|
||||||
it.build()
|
it.build()
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +170,6 @@ class RustMatrixClient constructor(
|
||||||
slidingSync,
|
slidingSync,
|
||||||
visibleRoomsSlidingSyncList,
|
visibleRoomsSlidingSyncList,
|
||||||
dispatchers,
|
dispatchers,
|
||||||
::onRestartSync
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override val roomSummaryDataSource: RoomSummaryDataSource
|
override val roomSummaryDataSource: RoomSummaryDataSource
|
||||||
|
|
@ -170,7 +181,6 @@ class RustMatrixClient constructor(
|
||||||
slidingSync,
|
slidingSync,
|
||||||
invitesSlidingSyncList,
|
invitesSlidingSyncList,
|
||||||
dispatchers,
|
dispatchers,
|
||||||
::onRestartSync
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override val invitesDataSource: RoomSummaryDataSource
|
override val invitesDataSource: RoomSummaryDataSource
|
||||||
|
|
@ -196,11 +206,6 @@ class RustMatrixClient constructor(
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRestartSync() {
|
|
||||||
stopSync()
|
|
||||||
startSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||||
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
|
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
|
||||||
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
|
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
|
||||||
|
|
@ -304,7 +309,10 @@ class RustMatrixClient constructor(
|
||||||
rustRoomSummaryDataSource.close()
|
rustRoomSummaryDataSource.close()
|
||||||
rustInvitesDataSource.close()
|
rustInvitesDataSource.close()
|
||||||
client.setDelegate(null)
|
client.setDelegate(null)
|
||||||
visibleRoomsSlidingSyncList.destroy()
|
visibleRoomsSlidingSyncListBuilder.destroy()
|
||||||
|
invitesSlidingSyncListBuilder.destroy()
|
||||||
|
visibleRoomsSlidingSyncList.resetReplayCache()
|
||||||
|
invitesSlidingSyncList.resetReplayCache()
|
||||||
slidingSync.destroy()
|
slidingSync.destroy()
|
||||||
verificationService.destroy()
|
verificationService.destroy()
|
||||||
client.destroy()
|
client.destroy()
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,12 @@ import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo
|
||||||
|
|
||||||
fun RustAudioInfo.map(): AudioInfo = AudioInfo(
|
fun RustAudioInfo.map(): AudioInfo = AudioInfo(
|
||||||
duration = duration?.toLong(),
|
duration = duration?.toLong(),
|
||||||
size = size?.toLong()
|
size = size?.toLong(),
|
||||||
|
mimeType = mimetype
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AudioInfo.map(): RustAudioInfo = RustAudioInfo(
|
||||||
|
duration = duration?.toULong(),
|
||||||
|
size = size?.toULong(),
|
||||||
|
mimetype = mimeType,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,10 @@ fun RustFileInfo.map(): FileInfo = FileInfo(
|
||||||
thumbnailInfo = thumbnailInfo?.map(),
|
thumbnailInfo = thumbnailInfo?.map(),
|
||||||
thumbnailSource = thumbnailSource?.map()
|
thumbnailSource = thumbnailSource?.map()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun FileInfo.map(): RustFileInfo = RustFileInfo(
|
||||||
|
mimetype = mimetype,
|
||||||
|
size = size?.toULong(),
|
||||||
|
thumbnailInfo = thumbnailInfo?.map(),
|
||||||
|
thumbnailSource = null
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package io.element.android.libraries.matrix.impl.media
|
package io.element.android.libraries.matrix.impl.media
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||||
|
import org.matrix.rustcomponents.sdk.MediaSource
|
||||||
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
|
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
|
||||||
|
|
||||||
fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
||||||
|
|
@ -28,3 +29,13 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
||||||
thumbnailSource = thumbnailSource?.map(),
|
thumbnailSource = thumbnailSource?.map(),
|
||||||
blurhash = blurhash
|
blurhash = blurhash
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun ImageInfo.map(): RustImageInfo = RustImageInfo(
|
||||||
|
height = height?.toULong(),
|
||||||
|
width = width?.toULong(),
|
||||||
|
mimetype = mimetype,
|
||||||
|
size = size?.toULong(),
|
||||||
|
thumbnailInfo = thumbnailInfo?.map(),
|
||||||
|
thumbnailSource = null,
|
||||||
|
blurhash = blurhash,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ class RustMediaLoader(
|
||||||
mediaSourceFromUrl(source.url).use { mediaSource ->
|
mediaSourceFromUrl(source.url).use { mediaSource ->
|
||||||
val mediaFile = innerClient.getMediaFile(
|
val mediaFile = innerClient.getMediaFile(
|
||||||
mediaSource = mediaSource,
|
mediaSource = mediaSource,
|
||||||
|
body = null,
|
||||||
mimeType = mimeType ?: "application/octet-stream"
|
mimeType = mimeType ?: "application/octet-stream"
|
||||||
)
|
)
|
||||||
RustMediaFile(mediaFile)
|
RustMediaFile(mediaFile)
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,10 @@ fun RustThumbnailInfo.map(): ThumbnailInfo = ThumbnailInfo(
|
||||||
mimetype = mimetype,
|
mimetype = mimetype,
|
||||||
size = size?.toLong()
|
size = size?.toLong()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun ThumbnailInfo.map(): RustThumbnailInfo = RustThumbnailInfo(
|
||||||
|
height = height?.toULong(),
|
||||||
|
width = width?.toULong(),
|
||||||
|
mimetype = mimetype,
|
||||||
|
size = size?.toULong()
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,14 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo(
|
||||||
thumbnailSource = thumbnailSource?.map(),
|
thumbnailSource = thumbnailSource?.map(),
|
||||||
blurhash = blurhash
|
blurhash = blurhash
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun VideoInfo.map(): RustVideoInfo = RustVideoInfo(
|
||||||
|
duration = duration?.toULong(),
|
||||||
|
height = height?.toULong(),
|
||||||
|
width = width?.toULong(),
|
||||||
|
mimetype = mimetype,
|
||||||
|
size = size?.toULong(),
|
||||||
|
thumbnailInfo = thumbnailInfo?.map(),
|
||||||
|
thumbnailSource = null,
|
||||||
|
blurhash = blurhash
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.notification
|
package io.element.android.libraries.matrix.impl.notification
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
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
|
||||||
|
|
@ -36,7 +37,7 @@ class NotificationMapper @Inject constructor() {
|
||||||
senderDisplayName = it.senderDisplayName,
|
senderDisplayName = it.senderDisplayName,
|
||||||
roomAvatarUrl = it.roomAvatarUrl,
|
roomAvatarUrl = it.roomAvatarUrl,
|
||||||
isDirect = it.isDirect,
|
isDirect = it.isDirect,
|
||||||
isEncrypted = it.isEncrypted,
|
isEncrypted = it.isEncrypted.orFalse(),
|
||||||
isNoisy = it.isNoisy
|
isNoisy = it.isNoisy
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue