Merge branch 'develop' into feature/fga/image_loading
This commit is contained in:
commit
4b49d40801
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"
|
||||
],
|
||||
"labels": ["dependencies"],
|
||||
"reviewers": ["team:element-x-android-reviewers"],
|
||||
"ignoreDeps": ["string:app_name"],
|
||||
"packageRules": [
|
||||
{
|
||||
|
|
|
|||
1
.github/workflows/nightly.yml
vendored
1
.github/workflows/nightly.yml
vendored
|
|
@ -1,6 +1,7 @@
|
|||
name: Build and release nightly APK
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Every nights at 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
|
||||
- launchApp:
|
||||
clearKeychain: true
|
||||
- tapOn: "Close showkase button"
|
||||
- runFlow: ./assertions/assertInitDisplayed.yaml
|
||||
- takeScreenshot: build/maestro/000-FirstScreen
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
appId: ${APP_ID}
|
||||
---
|
||||
- runFlow: ../assertions/assertRoomListSynced.yaml
|
||||
- tapOn: "search"
|
||||
- inputText: ${ROOM_NAME.substring(0, 3)}
|
||||
- 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)
|
||||
api(libs.anvil.compiler.api)
|
||||
implementation(libs.anvil.compiler.utils)
|
||||
implementation("com.squareup:kotlinpoet:1.13.1")
|
||||
implementation("com.squareup:kotlinpoet:1.13.2")
|
||||
implementation(libs.dagger)
|
||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0.1")
|
||||
kapt("com.google.auto.service:auto-service:1.0.1")
|
||||
|
|
|
|||
|
|
@ -29,15 +29,6 @@
|
|||
android:theme="@style/Theme.ElementX"
|
||||
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
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
|
||||
|
|
@ -66,6 +57,15 @@
|
|||
android:exported="false"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package io.element.android.appnav
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.composable.Children
|
||||
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.core.SessionId
|
||||
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.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -140,17 +137,11 @@ class RootFlowNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val activity = LocalContext.current as Activity
|
||||
fun openShowkase() {
|
||||
openShowkase(activity)
|
||||
}
|
||||
|
||||
val state = presenter.present()
|
||||
RootView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onOpenBugReport = this::onOpenBugReport,
|
||||
onOpenShowkase = ::openShowkase
|
||||
) {
|
||||
Children(
|
||||
navModel = backstack,
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
package io.element.android.appnav.root
|
||||
|
||||
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.detection.RageshakeDetectionPresenter
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
|
|
@ -31,23 +29,12 @@ class RootPresenter @Inject constructor(
|
|||
|
||||
@Composable
|
||||
override fun present(): RootState {
|
||||
val isShowkaseButtonVisible = rememberSaveable {
|
||||
mutableStateOf(true)
|
||||
}
|
||||
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
|
||||
val crashDetectionState = crashDetectionPresenter.present()
|
||||
|
||||
fun handleEvent(event: RootEvents) {
|
||||
when (event) {
|
||||
RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return RootState(
|
||||
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
|
||||
rageshakeDetectionState = rageshakeDetectionState,
|
||||
crashDetectionState = crashDetectionState,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionSta
|
|||
|
||||
@Immutable
|
||||
data class RootState(
|
||||
val isShowkaseButtonVisible: Boolean,
|
||||
val rageshakeDetectionState: RageshakeDetectionState,
|
||||
val crashDetectionState: CrashDetectionState,
|
||||
val eventSink: (RootEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,12 +24,10 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
|
|||
override val values: Sequence<RootState>
|
||||
get() = sequenceOf(
|
||||
aRootState().copy(
|
||||
isShowkaseButtonVisible = true,
|
||||
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = false),
|
||||
crashDetectionState = aCrashDetectionState().copy(crashDetected = true),
|
||||
),
|
||||
aRootState().copy(
|
||||
isShowkaseButtonVisible = true,
|
||||
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true),
|
||||
crashDetectionState = aCrashDetectionState().copy(crashDetected = false),
|
||||
)
|
||||
|
|
@ -37,8 +35,6 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
|
|||
}
|
||||
|
||||
fun aRootState() = RootState(
|
||||
isShowkaseButtonVisible = false,
|
||||
rageshakeDetectionState = aRageshakeDetectionState(),
|
||||
crashDetectionState = aCrashDetectionState(),
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ fun RootView(
|
|||
state: RootState,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenBugReport: () -> Unit = {},
|
||||
onOpenShowkase: () -> Unit = {},
|
||||
children: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -46,7 +45,6 @@ fun RootView(
|
|||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
children()
|
||||
val eventSink = state.eventSink
|
||||
|
||||
fun onOpenBugReport() {
|
||||
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
||||
|
|
@ -54,11 +52,6 @@ fun RootView(
|
|||
onOpenBugReport.invoke()
|
||||
}
|
||||
|
||||
ShowkaseButton(
|
||||
isVisible = state.isShowkaseButtonVisible,
|
||||
onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
|
||||
onClick = onOpenShowkase
|
||||
)
|
||||
RageshakeDetectionView(
|
||||
state = state.rageshakeDetectionState,
|
||||
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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.appnav
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
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.features.rageshake.impl.crash.DefaultCrashDetectionPresenter
|
||||
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.FakeRageshakeDataStore
|
||||
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
@ -44,21 +40,7 @@ class RootPresenterTest {
|
|||
}.test {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isShowkaseButtonVisible).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
assertThat(initialState.crashDetectionState.crashDetected).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.appnav.loggedin
|
||||
|
||||
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.providers.api.Distributor
|
||||
import io.element.android.libraries.push.providers.api.PushProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -285,12 +285,15 @@ tasks.register("runQualityChecks") {
|
|||
|
||||
// Make sure to delete old screenshots before recording new ones
|
||||
subprojects {
|
||||
val snapshotsDir = File("${project.path}/src/test/snapshots")
|
||||
val snapshotsDir = File("${project.projectDir}/src/test/snapshots")
|
||||
val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") {
|
||||
onlyIf { snapshotsDir.exists() }
|
||||
doFirst {
|
||||
println("Delete previous screenshots located at $snapshotsDir\n")
|
||||
snapshotsDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
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
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.bumble.appyx.core.composable.Children
|
|||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -76,10 +77,11 @@ class ConfigureRoomFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.ConfigureRoom)
|
||||
}
|
||||
}
|
||||
createNode<AddPeopleNode>(context = buildContext, plugins = plugins.plus(callback))
|
||||
createNode<AddPeopleNode>(context = buildContext, plugins = listOf(callback))
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
private val callback = object : Callback {
|
||||
override fun onCreateRoomSuccess(roomId: RoomId) {
|
||||
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
||||
}
|
||||
private fun onRoomCreated(roomId: RoomId) {
|
||||
plugins<Callback>().forEach { it.onCreateRoomSuccess(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -52,7 +50,7 @@ class ConfigureRoomNode @AssistedInject constructor(
|
|||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = this::navigateUp,
|
||||
onRoomCreated = callback::onCreateRoomSuccess
|
||||
onRoomCreated = this::onRoomCreated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,11 +65,12 @@ class CreateRoomRootPresenter @Inject constructor(
|
|||
|
||||
fun startDm(matrixUser: MatrixUser) {
|
||||
startDmAction.value = Async.Uninitialized
|
||||
val existingDM = matrixClient.findDM(matrixUser.userId)
|
||||
if (existingDM == null) {
|
||||
localCoroutineScope.createDM(matrixUser, startDmAction)
|
||||
} else {
|
||||
startDmAction.value = Async.Success(existingDM.roomId)
|
||||
matrixClient.findDM(matrixUser.userId).use { existingDM ->
|
||||
if (existingDM == null) {
|
||||
localCoroutineScope.createDM(matrixUser, startDmAction)
|
||||
} else {
|
||||
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_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_create_room_title">"Crear una sala"</string>
|
||||
</resources>
|
||||
|
|
@ -5,4 +5,5 @@
|
|||
<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_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>
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
<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_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_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_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>
|
||||
|
|
@ -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_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class AllMatrixUsersDataSourceTest {
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.createroom.impl.addpeople
|
||||
|
||||
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.test.FakeUserListDataSource
|
||||
import io.element.android.features.userlist.test.FakeUserListPresenterFactory
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.createroom.impl.configureroom
|
||||
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.createroom.impl.root
|
||||
|
||||
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.room.FakeMatrixRoom
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
|
|||
|
|
@ -131,14 +131,18 @@ class InviteListPresenter @Inject constructor(
|
|||
|
||||
private fun CoroutineScope.acceptInvite(roomId: RoomId, acceptedAction: MutableState<Async<RoomId>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.acceptInvitation()?.getOrThrow()
|
||||
client.getRoom(roomId)?.use {
|
||||
it.acceptInvitation().getOrThrow()
|
||||
}
|
||||
roomId
|
||||
}.execute(acceptedAction)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.declineInvite(roomId: RoomId, declinedAction: MutableState<Async<Unit>>) = launch {
|
||||
suspend {
|
||||
client.getRoom(roomId)?.rejectInvitation()?.getOrThrow() ?: Unit
|
||||
client.getRoom(roomId)?.use {
|
||||
it.rejectInvitation().getOrThrow()
|
||||
} ?: Unit
|
||||
}.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.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class InviteListPresenterTests {
|
||||
|
||||
@Test
|
||||
|
|
@ -509,5 +507,4 @@ class InviteListPresenterTests {
|
|||
unreadNotificationCount = 0,
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.login.impl.changeserver
|
||||
|
||||
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_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ChangeServerPresenterTest {
|
||||
@Test
|
||||
fun `present - should start with default homeserver`() = runTest {
|
||||
|
|
|
|||
|
|
@ -14,25 +14,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.login.impl.root
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
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_PASSWORD
|
||||
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_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_signout_confirmation_dialog_submit">"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_confirmation_dialog_submit">"Abmelden"</string>
|
||||
<string name="screen_signout_preference_item">"Abmelden"</string>
|
||||
</resources>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_submit">"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_confirmation_dialog_submit">"Cerrar sesión"</string>
|
||||
<string name="screen_signout_preference_item">"Cerrar sesión"</string>
|
||||
</resources>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_submit">"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_confirmation_dialog_submit">"Esci"</string>
|
||||
<string name="screen_signout_preference_item">"Esci"</string>
|
||||
</resources>
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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_submit">"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_confirmation_dialog_submit">"Deconectați-vă"</string>
|
||||
<string name="screen_signout_preference_item">"Deconectați-vă"</string>
|
||||
</resources>
|
||||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
|
||||
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_THROWABLE
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ dependencies {
|
|||
implementation(projects.libraries.textcomposer)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.dateformatter.api)
|
||||
implementation(projects.libraries.mediapickers)
|
||||
implementation(projects.libraries.mediapickers.api)
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.mediaupload.api)
|
||||
implementation(projects.features.networkmonitor.api)
|
||||
implementation(libs.coil.compose)
|
||||
implementation(libs.datetime)
|
||||
|
|
@ -64,6 +65,9 @@ dependencies {
|
|||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.libraries.featureflag.test)
|
||||
testImplementation(projects.libraries.mediaupload.test)
|
||||
testImplementation(projects.libraries.mediapickers.test)
|
||||
testImplementation(libs.test.mockk)
|
||||
|
||||
androidTestImplementation(libs.test.junitext)
|
||||
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.features.networkmonitor.api.NetworkMonitor
|
||||
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.launch
|
||||
import timber.log.Timber
|
||||
|
|
@ -52,6 +54,7 @@ class MessagesPresenter @Inject constructor(
|
|||
private val timelinePresenter: TimelinePresenter,
|
||||
private val actionListPresenter: ActionListPresenter,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
) : Presenter<MessagesState> {
|
||||
|
||||
@Composable
|
||||
|
|
@ -71,6 +74,8 @@ class MessagesPresenter @Inject constructor(
|
|||
|
||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState(initial = networkMonitor.currentConnectivityStatus)
|
||||
|
||||
val snackbarMessage = handleSnackbarMessage(snackbarDispatcher)
|
||||
|
||||
LaunchedEffect(syncUpdateFlow) {
|
||||
roomAvatar.value =
|
||||
AvatarData(
|
||||
|
|
@ -97,6 +102,7 @@ class MessagesPresenter @Inject constructor(
|
|||
timelineState = timelineState,
|
||||
actionListState = actionListState,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
snackbarMessage = snackbarMessage,
|
||||
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.timeline.TimelineState
|
||||
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
|
||||
|
||||
@Immutable
|
||||
|
|
@ -32,5 +33,6 @@ data class MessagesState(
|
|||
val timelineState: TimelineState,
|
||||
val actionListState: ActionListState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -52,5 +52,6 @@ fun aMessagesState() = MessagesState(
|
|||
),
|
||||
actionListState = anActionListState(),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import androidx.compose.material3.SnackbarHost
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -99,7 +100,6 @@ fun MessagesView(
|
|||
val itemActionsBottomSheetState = rememberModalBottomSheetState(
|
||||
initialValue = ModalBottomSheetValue.Hidden,
|
||||
)
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val composerState = state.composerState
|
||||
val initialBottomSheetState = if (LocalInspectionMode.current && composerState.attachmentSourcePicker != null) {
|
||||
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
|
||||
val localView = LocalView.current
|
||||
|
||||
|
|
|
|||
|
|
@ -27,23 +27,30 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
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.media.local.LocalMediaFactory
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.data.StableCharSequence
|
||||
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.SingleIn
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
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 kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import io.element.android.libraries.ui.strings.R as StringR
|
||||
|
||||
@SingleIn(RoomScope::class)
|
||||
class MessageComposerPresenter @Inject constructor(
|
||||
|
|
@ -52,6 +59,8 @@ class MessageComposerPresenter @Inject constructor(
|
|||
private val mediaPickerProvider: PickerProvider,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
private val localMediaFactory: LocalMediaFactory,
|
||||
private val mediaPreProcessor: MediaPreProcessor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
) : Presenter<MessageComposerState> {
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
|
|
@ -73,10 +82,12 @@ class MessageComposerPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { handlePickedMedia(it) })
|
||||
val filesPicker = mediaPickerProvider.registerFilePicker(onResult = { handlePickedMedia(it) })
|
||||
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 galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType ->
|
||||
handlePickedMedia(uri, mimeType)
|
||||
})
|
||||
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 {
|
||||
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.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages
|
||||
|
||||
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.timeline.TimelinePresenter
|
||||
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.matrix.api.room.MatrixRoom
|
||||
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.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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -132,8 +131,10 @@ class MessagesPresenterTest {
|
|||
val messageComposerPresenter = MessageComposerPresenter(
|
||||
appCoroutineScope = this,
|
||||
room = matrixRoom,
|
||||
mediaPickerProvider = PickerProvider(isInTest = true),
|
||||
mediaPickerProvider = FakePickerProvider(),
|
||||
featureFlagService = FakeFeatureFlagService(),
|
||||
mediaPreProcessor = FakeMediaPreProcessor(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
)
|
||||
val timelinePresenter = TimelinePresenter(
|
||||
timelineItemsFactory = aTimelineItemsFactory(),
|
||||
|
|
@ -146,6 +147,7 @@ class MessagesPresenterTest {
|
|||
timelinePresenter = timelinePresenter,
|
||||
actionListPresenter = actionListPresenter,
|
||||
networkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.actionlist
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
|
|
@ -23,9 +21,9 @@ import app.cash.molecule.moleculeFlow
|
|||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.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.TimelineItemReactions
|
||||
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_NAME
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.fixtures
|
||||
|
||||
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.libraries.dateformatter.test.FakeDaySeparatorFormatter
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
internal fun aTimelineItemsFactory() = TimelineItemsFactory(
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
|
|
|
|||
|
|
@ -22,45 +22,59 @@ import app.cash.molecule.RecompositionClock
|
|||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.ReceiveTurbine
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
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.MessageComposerPresenter
|
||||
import io.element.android.features.messages.impl.textcomposer.MessageComposerState
|
||||
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.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.AN_EVENT_ID
|
||||
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_USER_NAME
|
||||
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.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
|
||||
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 {
|
||||
runBlocking {
|
||||
setFeatureEnabled(FeatureFlags.ShowMediaUploadingFlow, true)
|
||||
}
|
||||
}
|
||||
private val mediaPreProcessor = FakeMediaPreProcessor()
|
||||
private val snackbarDispatcher = SnackbarDispatcher()
|
||||
|
||||
@Test
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -74,12 +88,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - toggle fullscreen`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -95,12 +104,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - change message`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -118,12 +122,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - change mode to edit`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.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
|
||||
fun `present - change mode to reply`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -172,12 +157,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - change mode to quote`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -194,12 +174,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - send message`() = runTest {
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
FakeMatrixRoom(),
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -218,11 +193,9 @@ class MessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - edit message`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
val presenter = createPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -251,11 +224,9 @@ class MessageComposerPresenterTest {
|
|||
@Test
|
||||
fun `present - reply message`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
val presenter = createPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
|
|
@ -283,13 +254,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - Open attachments menu`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -302,13 +267,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - Open camera attachments menu`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -321,13 +280,7 @@ class MessageComposerPresenterTest {
|
|||
|
||||
@Test
|
||||
fun `present - Dismiss attachments menu`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val presenter = createPresenter(this)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -341,80 +294,204 @@ class MessageComposerPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - Pick media from gallery`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
fun `present - Pick image from gallery`() = runTest {
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createPresenter(this, room = room)
|
||||
pickerProvider.givenMimeType(MimeTypes.Images)
|
||||
mediaPreProcessor.givenResult(
|
||||
Result.success(
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
fun `present - Pick file from storage`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
val room = FakeMatrixRoom()
|
||||
val presenter = createPresenter(this, room = room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
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) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
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
|
||||
fun `present - Take photo`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
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
|
||||
}
|
||||
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
|
||||
fun `present - Record video`() = runTest {
|
||||
val fakeMatrixRoom = FakeMatrixRoom()
|
||||
val presenter = MessageComposerPresenter(
|
||||
this,
|
||||
fakeMatrixRoom,
|
||||
pickerProvider,
|
||||
featureFlagService,
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
private fun createPresenter(
|
||||
coroutineScope: CoroutineScope,
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
pickerProvider: PickerProvider = this.pickerProvider,
|
||||
featureFlagService: FeatureFlagService = this.featureFlagService,
|
||||
mediaPreProcessor: MediaPreProcessor = this.mediaPreProcessor,
|
||||
snackbarDispatcher: SnackbarDispatcher = this.snackbarDispatcher,
|
||||
) = MessageComposerPresenter(
|
||||
coroutineScope, room, pickerProvider, featureFlagService, mediaPreProcessor, snackbarDispatcher
|
||||
)
|
||||
}
|
||||
|
||||
fun anEditMode() = MessageComposerMode.Edit(AN_EVENT_ID, A_MESSAGE)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.messages.timeline
|
||||
|
||||
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.libraries.matrix.test.AN_EVENT_ID
|
||||
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 org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@
|
|||
|
||||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.runtime.Composable
|
||||
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.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
|
|
@ -35,11 +38,21 @@ class DeveloperSettingsNode @AssistedInject constructor(
|
|||
|
||||
@Composable
|
||||
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()
|
||||
DeveloperSettingsView(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onBackPressed = this::navigateUp
|
||||
onOpenShowkase = ::openShowkase,
|
||||
onBackPressed = ::navigateUp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,28 +14,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.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.model.FeatureUiModel
|
||||
import io.element.android.libraries.ui.strings.R
|
||||
|
|
@ -43,45 +33,42 @@ import io.element.android.libraries.ui.strings.R
|
|||
@Composable
|
||||
fun DeveloperSettingsView(
|
||||
state: DeveloperSettingsState,
|
||||
modifier: Modifier = Modifier,
|
||||
onOpenShowkase: () -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.systemBarsPadding()
|
||||
.imePadding(),
|
||||
contentWindowInsets = WindowInsets.statusBars,
|
||||
topBar = {
|
||||
PreferenceTopAppBar(
|
||||
title = stringResource(id = R.string.common_developer_options),
|
||||
onBackPressed = onBackPressed,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
FeatureListContent(it, state)
|
||||
PreferenceView(
|
||||
modifier = modifier,
|
||||
onBackPressed = onBackPressed,
|
||||
title = stringResource(id = R.string.common_developer_options)
|
||||
) {
|
||||
// Note: this is OK to hardcode strings in this debug screen.
|
||||
PreferenceCategory(title = "Feature flags") {
|
||||
FeatureListContent(state)
|
||||
}
|
||||
)
|
||||
PreferenceCategory(title = "Showkase") {
|
||||
PreferenceText(
|
||||
title = "Open Showkase browser",
|
||||
onClick = onOpenShowkase
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FeatureListContent(
|
||||
paddingValues: PaddingValues,
|
||||
state: DeveloperSettingsState,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
fun onFeatureEnabled(feature: FeatureUiModel, isEnabled: Boolean) {
|
||||
state.eventSink(DeveloperSettingsEvents.UpdateEnabledFeature(feature, isEnabled))
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
FeatureListView(features = state.features, onCheckedChange = ::onFeatureEnabled)
|
||||
}
|
||||
FeatureListView(
|
||||
modifier = modifier,
|
||||
features = state.features,
|
||||
onCheckedChange = ::onFeatureEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
@ -98,6 +85,7 @@ fun DeveloperSettingsViewDarkPreview(@PreviewParameter(DeveloperSettingsStatePro
|
|||
private fun ContentToPreview(state: DeveloperSettingsState) {
|
||||
DeveloperSettingsView(
|
||||
state = state,
|
||||
onOpenShowkase = {},
|
||||
onBackPressed = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.preferences.impl.developer
|
||||
|
||||
import app.cash.molecule.RecompositionClock
|
||||
|
|
@ -24,7 +22,6 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
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.core.meta.BuildType
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
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_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_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>
|
||||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.rageshake.impl.bugreport
|
||||
|
||||
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.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.rageshake.impl.crash.ui
|
||||
|
||||
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.test.crash.A_CRASH_DATA
|
||||
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.rageshake.impl.detection
|
||||
|
||||
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.libraries.matrix.test.AN_EXCEPTION
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class RageshakeDetectionPresenterTest {
|
||||
@Test
|
||||
|
|
@ -101,7 +99,7 @@ class RageshakeDetectionPresenterTest {
|
|||
)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
}.test(timeout = 30.seconds) {
|
||||
skipItems(1)
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.isStarted).isFalse()
|
||||
|
|
@ -155,7 +153,7 @@ class RageshakeDetectionPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - screenshot then disable`() = runTest {
|
||||
fun `present - screenshot then disable`() = runTest(timeout = 1.seconds) {
|
||||
val screenshotHolder = FakeScreenshotHolder(screenshotUri = null)
|
||||
val rageshake = FakeRageShake(isAvailableValue = true)
|
||||
val rageshakeDataStore = FakeRageshakeDataStore(isEnabled = true)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.features.rageshake.impl.preferences
|
||||
|
||||
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.FakeRageShake
|
||||
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ dependencies {
|
|||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.elementresources)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.features.userlist.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
api(projects.features.roomdetails.api)
|
||||
implementation(libs.coil.compose)
|
||||
|
|
@ -51,7 +50,6 @@ dependencies {
|
|||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.userlist.impl)
|
||||
testImplementation(projects.features.userlist.test)
|
||||
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.RoomMember
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -124,7 +125,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
MatrixRoomMembersState.Unknown -> Async.Uninitialized
|
||||
is MatrixRoomMembersState.Pending -> Async.Loading(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
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
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.userlist.api.UserListDataSource
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
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.RoomMember
|
||||
import javax.inject.Named
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
interface RoomMemberBindsModule {
|
||||
|
||||
@Binds
|
||||
@Named("RoomMembers")
|
||||
fun bindRoomMemberUserListDataSource(dataSource: RoomUserListDataSource): UserListDataSource
|
||||
}
|
||||
|
||||
@Module
|
||||
@ContributesTo(RoomScope::class)
|
||||
object RoomMemberProvidesModule {
|
||||
object RoomMemberModule {
|
||||
|
||||
@Provides
|
||||
fun provideRoomMemberDetailsPresenterFactory(
|
||||
|
|
@ -16,27 +16,23 @@
|
|||
|
||||
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.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.MatrixRoomMembersState
|
||||
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.toMatrixUser
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.coroutines.flow.dropWhile
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomUserListDataSource @Inject constructor(
|
||||
class RoomMemberListDataSource @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
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
|
||||
.dropWhile { it !is MatrixRoomMembersState.Ready }
|
||||
.first()
|
||||
|
|
@ -50,11 +46,7 @@ class RoomUserListDataSource @Inject constructor(
|
|||
|| member.displayName?.contains(query, ignoreCase = true).orFalse()
|
||||
}
|
||||
}
|
||||
filteredMembers.map(RoomMember::toMatrixUser)
|
||||
}
|
||||
|
||||
override suspend fun getProfile(userId: UserId): MatrixUser? {
|
||||
return null
|
||||
filteredMembers
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,8 +16,7 @@
|
|||
|
||||
package io.element.android.features.roomdetails.impl.members
|
||||
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.features.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
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 androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
class RoomMemberListPresenter @Inject constructor(
|
||||
private val userListPresenterFactory: UserListPresenter.Factory,
|
||||
@Named("RoomMembers") private val userListDataSource: UserListDataSource,
|
||||
private val userListDataStore: UserListDataStore,
|
||||
private val room: MatrixRoom,
|
||||
private val roomMemberListDataSource: RoomMemberListDataSource,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : Presenter<RoomMemberListState> {
|
||||
|
||||
private val userListPresenter by lazy {
|
||||
userListPresenterFactory.create(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource,
|
||||
userListDataStore,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun present(): RoomMemberListState {
|
||||
val userListState = userListPresenter.present()
|
||||
val allUsers = remember { mutableStateOf<Async<ImmutableList<MatrixUser>>>(Async.Loading()) }
|
||||
var roomMembers by remember { mutableStateOf<Async<RoomMembers>>(Async.Loading()) }
|
||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||
var searchResults by remember {
|
||||
mutableStateOf<RoomMemberSearchResultState>(RoomMemberSearchResultState.NotSearching)
|
||||
}
|
||||
var isSearchActive by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
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(
|
||||
allUsers = allUsers.value,
|
||||
userListState = userListState,
|
||||
roomMembers = roomMembers,
|
||||
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
|
||||
|
||||
import io.element.android.features.userlist.api.UserListState
|
||||
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
|
||||
|
||||
data class RoomMemberListState(
|
||||
val allUsers: Async<ImmutableList<MatrixUser>>,
|
||||
val userListState: UserListState,
|
||||
val roomMembers: Async<RoomMembers>,
|
||||
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
|
||||
|
||||
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.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
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.room.RoomMembershipState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class RoomMemberListStateProvider : PreviewParameterProvider<RoomMemberListState> {
|
||||
override val values: Sequence<RoomMemberListState>
|
||||
get() = sequenceOf(
|
||||
aRoomMemberListState(allUsers = Async.Success(persistentListOf(aMatrixUser()))),
|
||||
aRoomMemberListState(allUsers = Async.Loading())
|
||||
aRoomMemberListState(
|
||||
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(
|
||||
searchResults: UserSearchResultState = UserSearchResultState.NotSearching,
|
||||
allUsers: Async<ImmutableList<MatrixUser>> = Async.Uninitialized,
|
||||
) =
|
||||
RoomMemberListState(
|
||||
userListState = aUserListState().copy(searchResults = searchResults),
|
||||
allUsers = allUsers,
|
||||
)
|
||||
roomMembers: Async<RoomMembers> = Async.Uninitialized,
|
||||
searchResults: RoomMemberSearchResultState = RoomMemberSearchResultState.NotSearching,
|
||||
) = RoomMemberListState(
|
||||
roomMembers = roomMembers,
|
||||
searchQuery = "",
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.stringResource
|
||||
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.sp
|
||||
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.isLoading
|
||||
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.preview.ElementPreviewDark
|
||||
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.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.SearchBar
|
||||
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.room.RoomMember
|
||||
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)
|
||||
@Composable
|
||||
fun RoomMemberListView(
|
||||
state: RoomMemberListState,
|
||||
onBackPressed: () -> Unit,
|
||||
onMemberSelected: (UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onBackPressed: () -> Unit = {},
|
||||
onMemberSelected: (UserId) -> Unit = {},
|
||||
) {
|
||||
|
||||
fun onUserSelected(user: MatrixUser) {
|
||||
onMemberSelected(user.userId)
|
||||
fun onUserSelected(roomMember: RoomMember) {
|
||||
onMemberSelected(roomMember.userId)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (!state.userListState.isSearchActive) {
|
||||
if (!state.isSearchActive) {
|
||||
RoomMemberListTopBar(onBackPressed = onBackPressed)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,33 +97,26 @@ fun RoomMemberListView(
|
|||
.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
UserListView(
|
||||
state = state.userListState,
|
||||
onUserSelected = ::onUserSelected,
|
||||
)
|
||||
Column {
|
||||
RoomMemberSearchBar(
|
||||
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.allUsers is Async.Success) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) {
|
||||
item {
|
||||
val memberCount = state.allUsers.state.count()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
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()) {
|
||||
if (!state.isSearchActive) {
|
||||
if (state.roomMembers is Async.Success) {
|
||||
RoomMemberList(
|
||||
roomMembers = state.roomMembers.state,
|
||||
onUserSelected = ::onUserSelected,
|
||||
)
|
||||
} else if (state.roomMembers.isLoading()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
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)
|
||||
@Composable
|
||||
fun RoomMemberListTopBar(
|
||||
private fun RoomMemberListTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
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
|
||||
@Composable
|
||||
fun RoomMemberListLightPreview(@PreviewParameter(RoomMemberListStateProvider::class) state: RoomMemberListState) =
|
||||
|
|
@ -147,5 +301,9 @@ fun RoomMemberListDarkPreview(@PreviewParameter(RoomMemberListStateProvider::cla
|
|||
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomMemberListState) {
|
||||
RoomMemberListView(state)
|
||||
RoomMemberListView(
|
||||
state = state,
|
||||
onBackPressed = {},
|
||||
onMemberSelected = {}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
</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_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_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>
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
<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_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_people_title">"Personas"</string>
|
||||
<string name="screen_room_details_security_title">"Seguridad"</string>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
</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_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_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>
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
<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_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_people_title">"Persone"</string>
|
||||
<string name="screen_room_details_security_title">"Sicurezza"</string>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
</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_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_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>
|
||||
|
|
@ -14,6 +13,7 @@
|
|||
<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_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_people_title">"Persoane"</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_title">"Message encryption enabled"</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_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>
|
||||
|
|
|
|||
|
|
@ -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.RoomDetailsPresenter
|
||||
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.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
|
@ -90,7 +92,7 @@ class RoomDetailsPresenterTests {
|
|||
val room = aMatrixRoom()
|
||||
val roomMembers = listOf(
|
||||
aRoomMember(A_USER_ID),
|
||||
aRoomMember(A_USER_ID_2),
|
||||
aRoomMember(A_USER_ID_2, membership = RoomMembershipState.INVITE),
|
||||
)
|
||||
val presenter = aRoomDetailsPresenter(room)
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
|
|
@ -112,7 +114,7 @@ class RoomDetailsPresenterTests {
|
|||
room.givenRoomMembersState(MatrixRoomMembersState.Ready(roomMembers))
|
||||
//skipItems(1)
|
||||
val successState = awaitItem()
|
||||
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(roomMembers.size))
|
||||
Truth.assertThat(successState.memberCount).isEqualTo(Async.Success(1))
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
|
|
@ -266,22 +268,3 @@ fun aMatrixRoom(
|
|||
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.turbine.test
|
||||
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.userlist.api.SelectionMode
|
||||
import io.element.android.features.userlist.api.UserListDataSource
|
||||
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.features.userlist.api.UserSearchResultState
|
||||
import io.element.android.features.userlist.impl.DefaultUserListPresenter
|
||||
import io.element.android.features.userlist.test.FakeUserListDataSource
|
||||
import io.element.android.features.roomdetails.impl.members.RoomMemberSearchResultState
|
||||
import io.element.android.features.roomdetails.impl.members.aRoomMemberList
|
||||
import io.element.android.features.roomdetails.impl.members.aVictor
|
||||
import io.element.android.features.roomdetails.impl.members.aWalter
|
||||
import io.element.android.libraries.architecture.Async
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
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.MatrixRoomMembersState
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.internal.toImmutableList
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class RoomMemberListPresenterTests {
|
||||
|
||||
private val testCoroutineDispatchers = testCoroutineDispatchers()
|
||||
|
||||
@Test
|
||||
fun `present - search is done automatically on start, but is async`() = runTest {
|
||||
val searchResult = listOf(aMatrixUser())
|
||||
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
|
||||
)
|
||||
fun `search is done automatically on start, but is async`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
moleculeFlow(RecompositionClock.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
Truth.assertThat(initialState.allUsers).isInstanceOf(Async.Loading::class.java)
|
||||
Truth.assertThat(initialState.userListState.isSearchActive).isFalse()
|
||||
Truth.assertThat(initialState.userListState.searchResults).isEqualTo(UserSearchResultState.NotSearching)
|
||||
Truth.assertThat(initialState.userListState.selectionMode).isEqualTo(SelectionMode.Single)
|
||||
Truth.assertThat(initialState.roomMembers).isInstanceOf(Async.Loading::class.java)
|
||||
Truth.assertThat(initialState.searchQuery).isEmpty()
|
||||
Truth.assertThat(initialState.searchResults).isEqualTo(RoomMemberSearchResultState.NotSearching)
|
||||
Truth.assertThat(initialState.isSearchActive).isFalse()
|
||||
|
||||
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 io.element.android.features.roomdetails.aMatrixClient
|
||||
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.RoomMemberDetailsPresenter
|
||||
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.aRoomSummaryFilled
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DefaultInviteStateDataSourceTest {
|
||||
|
||||
@Test
|
||||
|
|
@ -133,5 +131,4 @@ internal class DefaultInviteStateDataSourceTest {
|
|||
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.aRoomSummaryFilled
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class) class RoomListPresenterTests {
|
||||
class RoomListPresenterTests {
|
||||
|
||||
@Test
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import app.cash.molecule.RecompositionClock
|
||||
import app.cash.molecule.moleculeFlow
|
||||
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.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.components.aMatrixUser
|
||||
import io.mockk.coJustRun
|
||||
import io.mockk.mockkConstructor
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DefaultUserListPresenterTests {
|
||||
|
||||
private val userListDataSource = FakeUserListDataSource()
|
||||
|
|
@ -136,9 +131,6 @@ class DefaultUserListPresenterTests {
|
|||
|
||||
@Test
|
||||
fun `present - select a user`() = runTest {
|
||||
mockkConstructor(LazyListState::class)
|
||||
coJustRun { anyConstructed<LazyListState>().scrollToItem(index = any()) }
|
||||
|
||||
val presenter = DefaultUserListPresenter(
|
||||
UserListPresenterArgs(selectionMode = SelectionMode.Single),
|
||||
userListDataSource,
|
||||
|
|
@ -158,16 +150,15 @@ class DefaultUserListPresenterTests {
|
|||
assertThat(awaitItem().selectedUsers).containsExactly(userA)
|
||||
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userB))
|
||||
// the last added user should be presented first
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userB, userA)
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userA, userB)
|
||||
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userABis))
|
||||
initialState.eventSink(UserListEvents.AddToSelection(userC))
|
||||
// duplicated users should be ignored
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userB, userA)
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userA, userB, userC)
|
||||
|
||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userB))
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userC, userA)
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(userA, userC)
|
||||
initialState.eventSink(UserListEvents.RemoveFromSelection(userA))
|
||||
assertThat(awaitItem().selectedUsers).containsExactly(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
|
||||
# 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
|
||||
android.experimental.enableTestFixtures=true
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ molecule = "0.9.0"
|
|||
|
||||
# AndroidX
|
||||
material = "1.9.0"
|
||||
core = "1.10.0"
|
||||
core = "1.10.1"
|
||||
datastore = "1.0.0"
|
||||
constraintlayout = "2.1.4"
|
||||
recyclerview = "1.3.0"
|
||||
|
|
@ -24,7 +24,7 @@ compose_bom = "2023.04.01"
|
|||
composecompiler = "1.4.7"
|
||||
|
||||
# Coroutines
|
||||
coroutines = "1.6.4"
|
||||
coroutines = "1.7.1"
|
||||
|
||||
# Accompanist
|
||||
accompanist = "0.30.1"
|
||||
|
|
@ -44,7 +44,7 @@ stem = "2.3.0"
|
|||
sqldelight = "1.5.5"
|
||||
|
||||
# DI
|
||||
dagger = "2.46"
|
||||
dagger = "2.46.1"
|
||||
anvil = "2.4.5"
|
||||
|
||||
# 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_datastore_preferences = { module = "androidx.datastore:datastore-preferences", 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_recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||
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_truth = "com.google.truth:truth:1.1.3"
|
||||
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" }
|
||||
|
||||
# Others
|
||||
|
|
@ -131,7 +132,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
|||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.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-jvm = { module = "com.squareup.sqldelight:sqlite-driver", 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"
|
||||
unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1"
|
||||
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
|
||||
inject = "javax.inject:javax.inject:1"
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ dependencies {
|
|||
implementation(libs.timber)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.activity.activity)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(projects.libraries.core)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@
|
|||
package io.element.android.libraries.androidutils.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.InputStream
|
||||
import kotlin.math.min
|
||||
|
||||
fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
|
||||
outputStream().use { out ->
|
||||
|
|
@ -25,3 +31,71 @@ fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int
|
|||
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
|
||||
|
||||
import android.content.Context
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
fun File.safeDelete() {
|
||||
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 Gif = "image/gif"
|
||||
|
||||
const val Videos = "video/*"
|
||||
const val Mp4 = "video/mp4"
|
||||
|
||||
const val Audio = "audio/*"
|
||||
|
||||
const val Ogg = "audio/ogg"
|
||||
|
||||
const val PlainText = "text/plain"
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
|
||||
package io.element.android.libraries.featureflag.ui
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -34,14 +33,10 @@ fun FeatureListView(
|
|||
onCheckedChange: (FeatureUiModel, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
LazyColumn(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
items = features,
|
||||
key = { it.key }
|
||||
) { feature ->
|
||||
|
||||
features.forEach { feature ->
|
||||
fun onCheckedChange(isChecked: Boolean) {
|
||||
onCheckedChange(feature, isChecked)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@ package io.element.android.libraries.matrix.api.media
|
|||
|
||||
data class AudioInfo(
|
||||
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
|
||||
|
||||
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.SessionId
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
|
||||
interface MatrixRoom : Closeable {
|
||||
val sessionId: SessionId
|
||||
|
|
@ -67,6 +72,14 @@ interface MatrixRoom : Closeable {
|
|||
|
||||
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 acceptInvitation(): Result<Unit>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package io.element.android.libraries.matrix.api.room
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
|
||||
data class RoomMember(
|
||||
val userId: UserId,
|
||||
|
|
@ -30,12 +29,6 @@ data class RoomMember(
|
|||
val isIgnored: Boolean,
|
||||
)
|
||||
|
||||
fun RoomMember.toMatrixUser() = MatrixUser(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
)
|
||||
|
||||
enum class RoomMembershipState {
|
||||
BAN, INVITE, JOIN, KNOCK, LEAVE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl
|
||||
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
|
|
@ -54,7 +58,9 @@ import kotlinx.coroutines.withTimeout
|
|||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientDelegate
|
||||
import org.matrix.rustcomponents.sdk.RequiredState
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncList
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncMode
|
||||
import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
|
|
@ -81,7 +87,7 @@ class RustMatrixClient constructor(
|
|||
client = client,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
private val notificationService = RustNotificationService(baseDirectory, dispatchers)
|
||||
private val notificationService = RustNotificationService(client)
|
||||
private var slidingSyncUpdateJob: Job? = null
|
||||
|
||||
private val clientDelegate = object : ClientDelegate {
|
||||
|
|
@ -104,7 +110,8 @@ class RustMatrixClient constructor(
|
|||
notTags = emptyList()
|
||||
)
|
||||
|
||||
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
|
||||
private val visibleRoomsSlidingSyncList = MutableSharedFlow<SlidingSyncList>(replay = 1)
|
||||
private val visibleRoomsSlidingSyncListBuilder = SlidingSyncListBuilder("CurrentlyVisibleRooms")
|
||||
.timelineLimit(limit = 1u)
|
||||
.requiredState(
|
||||
requiredState = listOf(
|
||||
|
|
@ -114,16 +121,19 @@ class RustMatrixClient constructor(
|
|||
)
|
||||
)
|
||||
.filters(visibleRoomsSlidingSyncFilters)
|
||||
.name(name = "CurrentlyVisibleRooms")
|
||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||
.addRange(0u, 20u)
|
||||
.use {
|
||||
it.build()
|
||||
}
|
||||
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||
visibleRoomsSlidingSyncList.tryEmit(list)
|
||||
return list
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
.requiredState(
|
||||
requiredState = listOf(
|
||||
|
|
@ -133,20 +143,22 @@ class RustMatrixClient constructor(
|
|||
)
|
||||
)
|
||||
.filters(invitesSlidingSyncFilters)
|
||||
.name(name = "CurrentInvites")
|
||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||
.addRange(0u, 20u)
|
||||
.use {
|
||||
it.build()
|
||||
}
|
||||
.onceBuilt(object : SlidingSyncListOnceBuilt {
|
||||
override fun updateList(list: SlidingSyncList): SlidingSyncList {
|
||||
invitesSlidingSyncList.tryEmit(list)
|
||||
return list
|
||||
}
|
||||
})
|
||||
|
||||
private val slidingSync = client
|
||||
.slidingSync()
|
||||
.homeserver("https://slidingsync.lab.matrix.org")
|
||||
.withCommonExtensions()
|
||||
.storageKey("ElementX")
|
||||
.addList(visibleRoomsSlidingSyncList)
|
||||
.addList(invitesSlidingSyncList)
|
||||
.addList(visibleRoomsSlidingSyncListBuilder)
|
||||
.addList(invitesSlidingSyncListBuilder)
|
||||
.use {
|
||||
it.build()
|
||||
}
|
||||
|
|
@ -158,7 +170,6 @@ class RustMatrixClient constructor(
|
|||
slidingSync,
|
||||
visibleRoomsSlidingSyncList,
|
||||
dispatchers,
|
||||
::onRestartSync
|
||||
)
|
||||
|
||||
override val roomSummaryDataSource: RoomSummaryDataSource
|
||||
|
|
@ -170,7 +181,6 @@ class RustMatrixClient constructor(
|
|||
slidingSync,
|
||||
invitesSlidingSyncList,
|
||||
dispatchers,
|
||||
::onRestartSync
|
||||
)
|
||||
|
||||
override val invitesDataSource: RoomSummaryDataSource
|
||||
|
|
@ -196,11 +206,6 @@ class RustMatrixClient constructor(
|
|||
.launchIn(coroutineScope)
|
||||
}
|
||||
|
||||
private fun onRestartSync() {
|
||||
stopSync()
|
||||
startSync()
|
||||
}
|
||||
|
||||
override fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
val slidingSyncRoom = slidingSync.getRoom(roomId.value) ?: return null
|
||||
val fullRoom = slidingSyncRoom.fullRoom() ?: return null
|
||||
|
|
@ -304,7 +309,10 @@ class RustMatrixClient constructor(
|
|||
rustRoomSummaryDataSource.close()
|
||||
rustInvitesDataSource.close()
|
||||
client.setDelegate(null)
|
||||
visibleRoomsSlidingSyncList.destroy()
|
||||
visibleRoomsSlidingSyncListBuilder.destroy()
|
||||
invitesSlidingSyncListBuilder.destroy()
|
||||
visibleRoomsSlidingSyncList.resetReplayCache()
|
||||
invitesSlidingSyncList.resetReplayCache()
|
||||
slidingSync.destroy()
|
||||
verificationService.destroy()
|
||||
client.destroy()
|
||||
|
|
|
|||
|
|
@ -21,5 +21,12 @@ import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo
|
|||
|
||||
fun RustAudioInfo.map(): AudioInfo = AudioInfo(
|
||||
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(),
|
||||
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
|
||||
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import org.matrix.rustcomponents.sdk.MediaSource
|
||||
import org.matrix.rustcomponents.sdk.ImageInfo as RustImageInfo
|
||||
|
||||
fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
||||
|
|
@ -28,3 +29,13 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo(
|
|||
thumbnailSource = thumbnailSource?.map(),
|
||||
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 ->
|
||||
val mediaFile = innerClient.getMediaFile(
|
||||
mediaSource = mediaSource,
|
||||
body = null,
|
||||
mimeType = mimeType ?: "application/octet-stream"
|
||||
)
|
||||
RustMediaFile(mediaFile)
|
||||
|
|
|
|||
|
|
@ -25,3 +25,10 @@ fun RustThumbnailInfo.map(): ThumbnailInfo = ThumbnailInfo(
|
|||
mimetype = mimetype,
|
||||
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(),
|
||||
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
|
||||
|
||||
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.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
|
@ -36,7 +37,7 @@ class NotificationMapper @Inject constructor() {
|
|||
senderDisplayName = it.senderDisplayName,
|
||||
roomAvatarUrl = it.roomAvatarUrl,
|
||||
isDirect = it.isDirect,
|
||||
isEncrypted = it.isEncrypted,
|
||||
isEncrypted = it.isEncrypted.orFalse(),
|
||||
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