Merge branch 'develop' into feature/fga/better_timeline_scroll

This commit is contained in:
ganfra 2023-07-12 20:16:49 +02:00
commit 79b84e3029
40 changed files with 310 additions and 170 deletions

View file

@ -26,6 +26,9 @@ jobs:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17' java-version: '17'
- name: ⚙️ Run unit & screenshot tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: ⚙️ Run unit & screenshot tests, generate kover report - name: ⚙️ Run unit & screenshot tests, generate kover report
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true

View file

@ -37,6 +37,9 @@ jobs:
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit & screenshot tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES -Pci-build=true
- name: ⚙️ Run unit & screenshot tests, generate kover report - name: ⚙️ Run unit & screenshot tests, generate kover report
run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true run: ./gradlew koverMergedReport $CI_GRADLE_ARG_PROPERTIES -Pci-build=true

View file

@ -17,23 +17,24 @@ jobs:
project-url: https://github.com/orgs/vector-im/projects/43 project-url: https://github.com/orgs/vector-im/projects/43
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
# move_needs_info: move_needs_info:
# name: Move triaged needs info issues on board name: Move triaged needs info issues on board
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# steps: steps:
# - uses: actions/add-to-project@main - uses: actions/add-to-project@main
# id: addItem id: addItem
# with: with:
# project-url: https://github.com/orgs/vector-im/projects/91 project-url: https://github.com/orgs/vector-im/projects/91
# github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
# labeled: X-Needs-Info labeled: X-Needs-Info
# - name: Print itemId
# - uses: kalgurn/update-project-item-status@main run: echo ${{ steps.addItem.outputs.itemId }}
# with: - uses: kalgurn/update-project-item-status@main
# project-url: https://github.com/orgs/vector-im/projects/91 with:
# github-token: ${{ secrets.ELEMENT_BOT_TOKEN }} project-url: https://github.com/orgs/vector-im/projects/91
# item-id: ${{ steps.addItem.outputs.itemId }} github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
# status: "Needs info" item-id: ${{ steps.addItem.outputs.itemId }}
status: "Needs info"
ex_plorers: ex_plorers:
name: Add labelled issues to X-Plorer project name: Add labelled issues to X-Plorer project

View file

@ -52,8 +52,7 @@ class MainActivity : NodeComponentActivity() {
Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}")
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
appBindings = bindings<AppBindings>() appBindings = bindings()
appBindings.matrixClientsHolder().restore(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
setContent { setContent {
MainContent(appBindings) MainContent(appBindings)
@ -125,9 +124,4 @@ class MainActivity : NodeComponentActivity() {
super.onDestroy() super.onDestroy()
Timber.tag(loggerTag.value).w("onDestroy") Timber.tag(loggerTag.value).w("onDestroy")
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
bindings<AppBindings>().matrixClientsHolder().onSaveInstanceState(outState)
}
} }

View file

@ -17,13 +17,11 @@
package io.element.android.x.di package io.element.android.x.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import io.element.android.appnav.di.MatrixClientsHolder
import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun matrixClientsHolder(): MatrixClientsHolder
fun mainDaggerComponentOwner(): MainDaggerComponentsOwner fun mainDaggerComponentOwner(): MainDaggerComponentsOwner
fun snackbarDispatcher(): SnackbarDispatcher fun snackbarDispatcher(): SnackbarDispatcher
} }

View file

@ -37,10 +37,8 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import java.io.File import java.io.File
import java.util.concurrent.Executors
@Module @Module
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@ -99,7 +97,6 @@ object AppModule {
io = Dispatchers.IO, io = Dispatchers.IO,
computation = Dispatchers.Default, computation = Dispatchers.Default,
main = Dispatchers.Main, main = Dispatchers.Main,
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
) )
} }

View file

@ -154,8 +154,6 @@ class LoggedInFlowNode @AssistedInject constructor(
syncService.stopSync() syncService.stopSync()
}, },
onDestroy = { onDestroy = {
val imageLoaderFactory = bindings<MatrixUIBindings>().notLoggedInImageLoaderFactory()
Coil.setImageLoader(imageLoaderFactory)
plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) } plugins<LifecycleCallback>().forEach { it.onFlowReleased(id, inputs.matrixClient) }
appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id) appNavigationStateService.onLeavingSession(id)

View file

@ -19,6 +19,7 @@ package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import coil.Coil
import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.lifecycle.subscribe import com.bumble.appyx.core.lifecycle.subscribe
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
@ -34,8 +35,8 @@ import io.element.android.features.onboarding.api.OnBoardingEntryPoint
import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.BackstackNode
import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class NotLoggedInFlowNode @AssistedInject constructor( class NotLoggedInFlowNode @AssistedInject constructor(
@ -43,6 +44,7 @@ class NotLoggedInFlowNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val onBoardingEntryPoint: OnBoardingEntryPoint, private val onBoardingEntryPoint: OnBoardingEntryPoint,
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
) : BackstackNode<NotLoggedInFlowNode.NavTarget>( ) : BackstackNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.OnBoarding, initialElement = NavTarget.OnBoarding,
@ -51,10 +53,12 @@ class NotLoggedInFlowNode @AssistedInject constructor(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
) { ) {
init { override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { Timber.v("OnCreate") }, onCreate = {
onDestroy = { Timber.v("OnDestroy") } Coil.setImageLoader(notLoggedInImageLoaderFactory)
},
) )
} }

View file

@ -30,6 +30,7 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.node import com.bumble.appyx.core.node.node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.push
@ -90,10 +91,16 @@ class RootFlowNode @AssistedInject constructor(
) { ) {
override fun onBuilt() { override fun onBuilt() {
matrixClientsHolder.restore(buildContext.savedStateMap)
super.onBuilt() super.onBuilt()
observeLoggedInState() observeLoggedInState()
} }
override fun onSaveInstanceState(state: MutableSavedStateMap) {
super.onSaveInstanceState(state)
matrixClientsHolder.save(state)
}
private fun observeLoggedInState() { private fun observeLoggedInState() {
combine( combine(
cacheService.onClearedCacheEventFlow(), cacheService.onClearedCacheEventFlow(),

View file

@ -16,13 +16,11 @@
package io.element.android.appnav.di package io.element.android.appnav.di
import android.os.Bundle import com.bumble.appyx.core.state.MutableSavedStateMap
import io.element.android.libraries.di.AppScope import com.bumble.appyx.core.state.SavedStateMap
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -30,7 +28,6 @@ import javax.inject.Inject
private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey"
@SingleIn(AppScope::class)
class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) {
private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>() private val sessionIdsToMatrixClient = ConcurrentHashMap<SessionId, MatrixClient>()
@ -55,16 +52,20 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
return sessionIdsToMatrixClient[sessionId] return sessionIdsToMatrixClient[sessionId]
} }
@Suppress("DEPRECATION") @Suppress("UNCHECKED_CAST")
fun restore(savedInstanceState: Bundle?) { fun restore(state: SavedStateMap?) {
if (savedInstanceState == null || sessionIdsToMatrixClient.isNotEmpty()) return Timber.d("Restore state")
val userIds = savedInstanceState.getSerializable(SAVE_INSTANCE_KEY) as? Array<UserId> if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also {
if (userIds.isNullOrEmpty()) return Timber.w("Restore with non-empty map")
}
val sessionIds = state[SAVE_INSTANCE_KEY] as? Array<SessionId>
Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}")
if (sessionIds.isNullOrEmpty()) return
// Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs.
runBlocking { runBlocking {
userIds.forEach { userId -> sessionIds.forEach { sessionId ->
Timber.v("Restore matrix session: $userId") Timber.d("Restore matrix session: $sessionId")
authenticationService.restoreSession(userId) authenticationService.restoreSession(sessionId)
.onSuccess { matrixClient -> .onSuccess { matrixClient ->
add(matrixClient) add(matrixClient)
} }
@ -75,9 +76,9 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService:
} }
} }
fun onSaveInstanceState(outState: Bundle) { fun save(state: MutableSavedStateMap) {
val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray()
Timber.v("Save matrix session keys = $sessionKeys") Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}")
outState.putSerializable(SAVE_INSTANCE_KEY, sessionKeys) state[SAVE_INSTANCE_KEY] = sessionKeys
} }
} }

View file

@ -65,7 +65,6 @@ class ForwardMessagesPresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
skipItems(1) skipItems(1)
val summary = aRoomSummaryDetail()
initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive) initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive)
assertThat(awaitItem().isSearchActive).isTrue() assertThat(awaitItem().isSearchActive).isTrue()

View file

@ -25,28 +25,65 @@ import androidx.compose.ui.res.stringResource
import io.element.android.features.roomdetails.impl.R import io.element.android.features.roomdetails.impl.R
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.RetryDialog
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory 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.PreferenceText
import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) { internal fun BlockUserSection(state: RoomMemberDetailsState, modifier: Modifier = Modifier) {
PreferenceCategory(showDivider = false, modifier = modifier) { PreferenceCategory(showDivider = false, modifier = modifier) {
if (state.isBlocked) { when (state.isBlocked) {
PreferenceText( is Async.Failure -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = false, eventSink = state.eventSink)
title = stringResource(R.string.screen_dm_details_unblock_user), is Async.Loading -> PreferenceBlockUser(isBlocked = state.isBlocked.prevData, isLoading = true, eventSink = state.eventSink)
icon = Icons.Outlined.Block, is Async.Success -> PreferenceBlockUser(isBlocked = state.isBlocked.data, isLoading = false, eventSink = state.eventSink)
onClick = { state.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) }, Async.Uninitialized -> PreferenceBlockUser(isBlocked = null, isLoading = true, eventSink = state.eventSink)
)
} else {
PreferenceText(
title = stringResource(R.string.screen_dm_details_block_user),
icon = Icons.Outlined.Block,
tintColor = MaterialTheme.colorScheme.error,
onClick = { state.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) },
)
} }
} }
if (state.isBlocked is Async.Failure) {
RetryDialog(
content = stringResource(CommonStrings.error_unknown),
onDismiss = { state.eventSink(RoomMemberDetailsEvents.ClearBlockUserError) },
onRetry = {
val event = when (state.isBlocked.prevData) {
true -> RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)
false -> RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)
null -> /*Should not happen */ RoomMemberDetailsEvents.ClearBlockUserError
}
state.eventSink(event)
},
)
}
}
@Composable
private fun PreferenceBlockUser(
isBlocked: Boolean?,
isLoading: Boolean,
eventSink: (RoomMemberDetailsEvents) -> Unit,
modifier: Modifier = Modifier,
) {
if (isBlocked.orFalse()) {
PreferenceText(
title = stringResource(R.string.screen_dm_details_unblock_user),
icon = Icons.Outlined.Block,
onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = true)) },
loadingCurrentValue = isLoading,
modifier = modifier,
)
} else {
PreferenceText(
title = stringResource(R.string.screen_dm_details_block_user),
icon = Icons.Outlined.Block,
tintColor = MaterialTheme.colorScheme.error,
onClick = { if (!isLoading) eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = true)) },
loadingCurrentValue = isLoading,
modifier = modifier,
)
}
} }
@Composable @Composable

View file

@ -19,5 +19,6 @@ package io.element.android.features.roomdetails.impl.members.details
sealed interface RoomMemberDetailsEvents { sealed interface RoomMemberDetailsEvents {
data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class BlockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents data class UnblockUser(val needsConfirmation: Boolean = false) : RoomMemberDetailsEvents
object ClearBlockUserError : RoomMemberDetailsEvents
object ClearConfirmationDialog : RoomMemberDetailsEvents object ClearConfirmationDialog : RoomMemberDetailsEvents
} }

View file

@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState.ConfirmationDialog
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
@ -53,8 +54,13 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) } var confirmationDialog by remember { mutableStateOf<ConfirmationDialog?>(null) }
val roomMember by room.getRoomMemberAsState(roomMemberId) val roomMember by room.getRoomMemberAsState(roomMemberId)
// the room member is not really live... // the room member is not really live...
val isBlocked = remember { val isBlocked: MutableState<Async<Boolean>> = remember(roomMember) {
mutableStateOf(roomMember?.isIgnored.orFalse()) val isIgnored = roomMember?.isIgnored
if (isIgnored == null) {
mutableStateOf(Async.Uninitialized)
} else {
mutableStateOf(Async.Success(isIgnored))
}
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
room.updateMembers() room.updateMembers()
@ -79,6 +85,9 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
} }
} }
RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null RoomMemberDetailsEvents.ClearConfirmationDialog -> confirmationDialog = null
RoomMemberDetailsEvents.ClearBlockUserError -> {
isBlocked.value = Async.Success(isBlocked.value.dataOrNull().orFalse())
}
} }
} }
@ -105,20 +114,31 @@ class RoomMemberDetailsPresenter @AssistedInject constructor(
) )
} }
private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch { private fun CoroutineScope.blockUser(userId: UserId, isBlockedState: MutableState<Async<Boolean>>) = launch {
isBlockedState.value = Async.Loading(false)
client.ignoreUser(userId) client.ignoreUser(userId)
.map { .fold(
isBlockedState.value = true onSuccess = {
room.updateMembers() isBlockedState.value = Async.Success(true)
} room.updateMembers()
},
onFailure = {
isBlockedState.value = Async.Failure(it, false)
}
)
} }
private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<Boolean>) = launch { private fun CoroutineScope.unblockUser(userId: UserId, isBlockedState: MutableState<Async<Boolean>>) = launch {
isBlockedState.value = Async.Loading(true)
client.unignoreUser(userId) client.unignoreUser(userId)
.map { .fold(
isBlockedState.value = false onSuccess = {
room.updateMembers() isBlockedState.value = Async.Success(false)
} room.updateMembers()
},
onFailure = {
isBlockedState.value = Async.Failure(it, true)
}
)
} }
} }

View file

@ -16,11 +16,13 @@
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.roomdetails.impl.members.details
import io.element.android.libraries.architecture.Async
data class RoomMemberDetailsState( data class RoomMemberDetailsState(
val userId: String, val userId: String,
val userName: String?, val userName: String?,
val avatarUrl: String?, val avatarUrl: String?,
val isBlocked: Boolean, val isBlocked: Async<Boolean>,
val displayConfirmationDialog: ConfirmationDialog? = null, val displayConfirmationDialog: ConfirmationDialog? = null,
val isCurrentUser: Boolean, val isCurrentUser: Boolean,
val eventSink: (RoomMemberDetailsEvents) -> Unit val eventSink: (RoomMemberDetailsEvents) -> Unit

View file

@ -17,15 +17,17 @@
package io.element.android.features.roomdetails.impl.members.details package io.element.android.features.roomdetails.impl.members.details
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.Async
open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> { open class RoomMemberDetailsStateProvider : PreviewParameterProvider<RoomMemberDetailsState> {
override val values: Sequence<RoomMemberDetailsState> override val values: Sequence<RoomMemberDetailsState>
get() = sequenceOf( get() = sequenceOf(
aRoomMemberDetailsState(), aRoomMemberDetailsState(),
aRoomMemberDetailsState().copy(userName = null), aRoomMemberDetailsState().copy(userName = null),
aRoomMemberDetailsState().copy(isBlocked = true), aRoomMemberDetailsState().copy(isBlocked = Async.Success(true)),
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block), aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block),
aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock), aRoomMemberDetailsState().copy(displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock),
aRoomMemberDetailsState().copy(isBlocked = Async.Loading(true)),
// Add other states here // Add other states here
) )
} }
@ -34,7 +36,7 @@ fun aRoomMemberDetailsState() = RoomMemberDetailsState(
userId = "@daniel:domain.com", userId = "@daniel:domain.com",
userName = "Daniel", userName = "Daniel",
avatarUrl = null, avatarUrl = null,
isBlocked = false, isBlocked = Async.Success(false),
isCurrentUser = false, isCurrentUser = false,
eventSink = {}, eventSink = {},
) )

View file

@ -25,7 +25,9 @@ import io.element.android.features.roomdetails.impl.members.aRoomMember
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -50,7 +52,7 @@ class RoomMemberDetailsPresenterTests {
Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId.value) Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId.value)
Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName) Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName)
Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl)
Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored) Truth.assertThat(initialState.isBlocked).isEqualTo(Async.Success(roomMember.isIgnored))
skipItems(1) skipItems(1)
val loadedState = awaitItem() val loadedState = awaitItem()
Truth.assertThat(loadedState.userName).isEqualTo("A custom name") Truth.assertThat(loadedState.userName).isEqualTo("A custom name")
@ -129,10 +131,33 @@ class RoomMemberDetailsPresenterTests {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false)) initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
Truth.assertThat(awaitItem().isBlocked).isTrue() Truth.assertThat(awaitItem().isBlocked.isLoading()).isTrue()
Truth.assertThat(awaitItem().isBlocked.dataOrNull()).isTrue()
initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false)) initialState.eventSink(RoomMemberDetailsEvents.UnblockUser(needsConfirmation = false))
Truth.assertThat(awaitItem().isBlocked).isFalse() Truth.assertThat(awaitItem().isBlocked.isLoading()).isTrue()
Truth.assertThat(awaitItem().isBlocked.dataOrNull()).isFalse()
}
}
@Test
fun `present - BlockUser with error`() = runTest {
val room = aMatrixRoom()
val roomMember = aRoomMember()
val matrixClient = FakeMatrixClient()
matrixClient.givenIgnoreUserResult(Result.failure(A_THROWABLE))
val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(RoomMemberDetailsEvents.BlockUser(needsConfirmation = false))
Truth.assertThat(awaitItem().isBlocked.isLoading()).isTrue()
val errorState = awaitItem()
Truth.assertThat(errorState.isBlocked.errorOrNull()).isEqualTo(A_THROWABLE)
// Clear error
initialState.eventSink(RoomMemberDetailsEvents.ClearBlockUserError)
Truth.assertThat(awaitItem().isBlocked).isEqualTo(Async.Success(false))
} }
} }

View file

@ -19,6 +19,7 @@ package io.element.android.libraries.architecture
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.bumble.appyx.core.children.ChildEntry import com.bumble.appyx.core.children.ChildEntry
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.BackStack
@ -39,4 +40,15 @@ abstract class BackstackNode<NavTarget : Any>(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins, plugins = plugins,
childKeepMode = childKeepMode, childKeepMode = childKeepMode,
) ) {
override fun onBuilt() {
super.onBuilt()
lifecycle.logLifecycle(this::class.java.simpleName)
whenChildAttached<Node> { _, child ->
// BackstackNode will be logged by their parent.
if (child !is BackstackNode<*>) {
child.lifecycle.logLifecycle(child::class.java.simpleName)
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.architecture
import androidx.lifecycle.Lifecycle
import com.bumble.appyx.core.lifecycle.subscribe
import timber.log.Timber
fun Lifecycle.logLifecycle(name: String) {
subscribe(
onCreate = { Timber.tag("Lifecycle").d("onCreate $name") },
onPause = { Timber.tag("Lifecycle").d("onPause $name") },
onResume = { Timber.tag("Lifecycle").d("onResume $name") },
onDestroy = { Timber.tag("Lifecycle").d("onDestroy $name") },
)
}

View file

@ -22,5 +22,4 @@ data class CoroutineDispatchers(
val io: CoroutineDispatcher, val io: CoroutineDispatcher,
val computation: CoroutineDispatcher, val computation: CoroutineDispatcher,
val main: CoroutineDispatcher, val main: CoroutineDispatcher,
val diffUpdateDispatcher: CoroutineDispatcher,
) )

View file

@ -64,7 +64,7 @@ fun String?.insertBeforeLast(insert: String, delimiter: String = "."): String {
* Throws if length is < 1. * Throws if length is < 1.
*/ */
fun String.ellipsize(length: Int): String { fun String.ellipsize(length: Int): String {
require(length > 1) require(length >= 1)
if (this.length <= length) { if (this.length <= length) {
return this return this

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.deeplink
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.core.ThreadId
@ -37,21 +36,15 @@ class DeeplinkParser @Inject constructor() {
if (host != HOST) return null if (host != HOST) return null
val pathBits = path.orEmpty().split("/").drop(1) val pathBits = path.orEmpty().split("/").drop(1)
val sessionId = pathBits.elementAtOrNull(0)?.let(::SessionId) ?: return null val sessionId = pathBits.elementAtOrNull(0)?.let(::SessionId) ?: return null
val screenPathComponent = pathBits.elementAtOrNull(1)
val roomId = tryOrNull { screenPathComponent?.let(::RoomId) }
return when { return when (val screenPathComponent = pathBits.elementAtOrNull(1)) {
roomId != null -> { null -> DeeplinkData.Root(sessionId)
DeepLinkPaths.INVITE_LIST -> DeeplinkData.InviteList(sessionId)
else -> {
val roomId = screenPathComponent.let(::RoomId)
val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId) val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId)
DeeplinkData.Room(sessionId, roomId, threadId) DeeplinkData.Room(sessionId, roomId, threadId)
} }
screenPathComponent == DeepLinkPaths.INVITE_LIST -> {
DeeplinkData.InviteList(sessionId)
}
screenPathComponent == null -> {
DeeplinkData.Root(sessionId)
}
else -> null
} }
} }
} }

View file

@ -34,7 +34,7 @@ class RoomMembershipContentFormatter @Inject constructor(
): CharSequence? { ): CharSequence? {
val userId = membershipContent.userId val userId = membershipContent.userId
val memberIsYou = matrixClient.isMe(userId) val memberIsYou = matrixClient.isMe(userId)
return when (val change = membershipContent.change) { return when (membershipContent.change) {
MembershipChange.JOINED -> if (memberIsYou) { MembershipChange.JOINED -> if (memberIsYou) {
sp.getString(R.string.state_event_room_join_by_you) sp.getString(R.string.state_event_room_join_by_you)
} else { } else {

View file

@ -86,7 +86,7 @@ interface MatrixRoom : Closeable {
suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit>
suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit> suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit>
suspend fun retrySendMessage(transactionId: String): Result<Unit> suspend fun retrySendMessage(transactionId: String): Result<Unit>

View file

@ -38,12 +38,13 @@ interface RoomSummaryDataSource {
suspend fun RoomSummaryDataSource.awaitAllRoomsAreLoaded(timeout: Duration = Duration.INFINITE) { suspend fun RoomSummaryDataSource.awaitAllRoomsAreLoaded(timeout: Duration = Duration.INFINITE) {
try { try {
Timber.d("awaitAllRoomsAreLoaded: wait")
withTimeout(timeout) { withTimeout(timeout) {
allRoomsLoadingState().firstOrNull { allRoomsLoadingState().firstOrNull {
it is RoomSummaryDataSource.LoadingState.Loaded it is RoomSummaryDataSource.LoadingState.Loaded
} }
} }
} catch (timeoutException: TimeoutCancellationException) { } catch (timeoutException: TimeoutCancellationException) {
Timber.v("AwaitAllRooms: no response after $timeout") Timber.d("awaitAllRoomsAreLoaded: no response after $timeout")
} }
} }

View file

@ -54,7 +54,7 @@ import io.element.android.libraries.matrix.impl.verification.RustSessionVerifica
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -73,6 +73,7 @@ import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParam
import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset
import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility
@OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixClient constructor( class RustMatrixClient constructor(
private val client: Client, private val client: Client,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
@ -85,6 +86,7 @@ class RustMatrixClient constructor(
override val sessionId: UserId = UserId(client.userId()) override val sessionId: UserId = UserId(client.userId())
private val roomListService = client.roomListServiceWithEncryption() private val roomListService = client.roomListServiceWithEncryption()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}") private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService() private val verificationService = RustSessionVerificationService()
private val syncService = RustSyncService(roomListService, sessionCoroutineScope) private val syncService = RustSyncService(roomListService, sessionCoroutineScope)
@ -92,6 +94,7 @@ class RustMatrixClient constructor(
client = client, client = client,
dispatchers = dispatchers, dispatchers = dispatchers,
) )
private val notificationService = RustNotificationService(client) private val notificationService = RustNotificationService(client)
private val clientDelegate = object : ClientDelegate { private val clientDelegate = object : ClientDelegate {
@ -105,7 +108,7 @@ class RustMatrixClient constructor(
RustRoomSummaryDataSource( RustRoomSummaryDataSource(
roomListService = roomListService, roomListService = roomListService,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
coroutineDispatchers = dispatchers, dispatcher = sessionDispatcher,
) )
override val roomSummaryDataSource: RoomSummaryDataSource override val roomSummaryDataSource: RoomSummaryDataSource
@ -150,7 +153,7 @@ class RustMatrixClient constructor(
) )
} }
private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? = withContext(dispatchers.io) { private suspend fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? = withContext(sessionDispatcher) {
val cachedRoomListItem = roomListService.roomOrNull(roomId.value) val cachedRoomListItem = roomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom() val fullRoom = cachedRoomListItem?.fullRoom()
if (cachedRoomListItem == null || fullRoom == null) { if (cachedRoomListItem == null || fullRoom == null) {
@ -165,19 +168,19 @@ class RustMatrixClient constructor(
return roomId?.let { getRoom(it) } return roomId?.let { getRoom(it) }
} }
override suspend fun ignoreUser(userId: UserId): Result<Unit> = withContext(dispatchers.io) { override suspend fun ignoreUser(userId: UserId): Result<Unit> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.ignoreUser(userId.value) client.ignoreUser(userId.value)
} }
} }
override suspend fun unignoreUser(userId: UserId): Result<Unit> = withContext(dispatchers.io) { override suspend fun unignoreUser(userId: UserId): Result<Unit> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.unignoreUser(userId.value) client.unignoreUser(userId.value)
} }
} }
override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = withContext(dispatchers.io) { override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId> = withContext(sessionDispatcher) {
runCatching { runCatching {
val rustParams = RustCreateRoomParameters( val rustParams = RustCreateRoomParameters(
name = createRoomParams.name, name = createRoomParams.name,
@ -221,14 +224,14 @@ class RustMatrixClient constructor(
return createRoom(createRoomParams) return createRoom(createRoomParams)
} }
override suspend fun getProfile(userId: UserId): Result<MatrixUser> = withContext(Dispatchers.IO) { override suspend fun getProfile(userId: UserId): Result<MatrixUser> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.getProfile(userId.value).let(UserProfileMapper::map) client.getProfile(userId.value).let(UserProfileMapper::map)
} }
} }
override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> = override suspend fun searchUsers(searchTerm: String, limit: Long): Result<MatrixSearchUserResults> =
withContext(dispatchers.io) { withContext(sessionDispatcher) {
runCatching { runCatching {
client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map) client.searchUsers(searchTerm, limit.toULong()).let(UserSearchResultMapper::map)
} }
@ -260,7 +263,7 @@ class RustMatrixClient constructor(
baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false) baseDirectory.deleteSessionDirectory(userID = sessionId.value, deleteCryptoDb = false)
} }
override suspend fun logout() = withContext(dispatchers.io) { override suspend fun logout() = withContext(sessionDispatcher) {
try { try {
client.logout() client.logout()
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -271,20 +274,20 @@ class RustMatrixClient constructor(
sessionStore.removeSession(sessionId.value) sessionStore.removeSession(sessionId.value)
} }
override suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) { override suspend fun loadUserDisplayName(): Result<String> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.displayName() client.displayName()
} }
} }
override suspend fun loadUserAvatarURLString(): Result<String?> = withContext(dispatchers.io) { override suspend fun loadUserAvatarURLString(): Result<String?> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.avatarUrl() client.avatarUrl()
} }
} }
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(dispatchers.io) { override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> = withContext(sessionDispatcher) {
runCatching { runCatching {
client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher()) client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher())
} }
@ -305,7 +308,7 @@ class RustMatrixClient constructor(
private suspend fun File.getCacheSize( private suspend fun File.getCacheSize(
userID: String, userID: String,
includeCryptoDb: Boolean = false, includeCryptoDb: Boolean = false,
): Long = withContext(dispatchers.io) { ): Long = withContext(sessionDispatcher) {
// Rust sanitises the user ID replacing invalid characters with an _ // Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_") val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this@getCacheSize, sanitisedUserID) val sessionDirectory = File(this@getCacheSize, sanitisedUserID)
@ -327,7 +330,7 @@ class RustMatrixClient constructor(
private suspend fun File.deleteSessionDirectory( private suspend fun File.deleteSessionDirectory(
userID: String, userID: String,
deleteCryptoDb: Boolean = false, deleteCryptoDb: Boolean = false,
): Boolean = withContext(dispatchers.io) { ): Boolean = withContext(sessionDispatcher) {
// Rust sanitises the user ID replacing invalid characters with an _ // Rust sanitises the user ID replacing invalid characters with an _
val sanitisedUserID = userID.replace(":", "_") val sanitisedUserID = userID.replace(":", "_")
val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID) val sessionDirectory = File(this@deleteSessionDirectory, sanitisedUserID)

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
@ -29,10 +30,12 @@ import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
class RustMediaLoader( class RustMediaLoader(
baseCacheDirectory: File, baseCacheDirectory: File,
private val dispatchers: CoroutineDispatchers, dispatchers: CoroutineDispatchers,
private val innerClient: Client, private val innerClient: Client,
) : MatrixMediaLoader { ) : MatrixMediaLoader {
@OptIn(ExperimentalCoroutinesApi::class)
private val mediaDispatcher = dispatchers.io.limitedParallelism(32)
private val cacheDirectory = File(baseCacheDirectory, "temp/media").apply { private val cacheDirectory = File(baseCacheDirectory, "temp/media").apply {
if (!exists()) { if (!exists()) {
mkdirs() mkdirs()
@ -41,7 +44,7 @@ class RustMediaLoader(
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> = override suspend fun loadMediaContent(source: MediaSource): Result<ByteArray> =
withContext(dispatchers.io) { withContext(mediaDispatcher) {
runCatching { runCatching {
source.toRustMediaSource().use { source -> source.toRustMediaSource().use { source ->
innerClient.getMediaContent(source).toUByteArray().toByteArray() innerClient.getMediaContent(source).toUByteArray().toByteArray()
@ -55,7 +58,7 @@ class RustMediaLoader(
width: Long, width: Long,
height: Long height: Long
): Result<ByteArray> = ): Result<ByteArray> =
withContext(dispatchers.io) { withContext(mediaDispatcher) {
runCatching { runCatching {
source.toRustMediaSource().use { mediaSource -> source.toRustMediaSource().use { mediaSource ->
innerClient.getMediaThumbnail( innerClient.getMediaThumbnail(
@ -68,7 +71,7 @@ class RustMediaLoader(
} }
override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result<MediaFile> = override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result<MediaFile> =
withContext(dispatchers.io) { withContext(mediaDispatcher) {
runCatching { runCatching {
source.toRustMediaSource().use { mediaSource -> source.toRustMediaSource().use { mediaSource ->
val mediaFile = innerClient.getMediaFile( val mediaFile = innerClient.getMediaFile(

View file

@ -62,7 +62,7 @@ fun RoomListService.roomOrNull(roomId: String): RoomListItem? {
return try { return try {
room(roomId) room(roomId)
} catch (exception: RoomListException) { } catch (exception: RoomListException) {
Timber.e(exception, "Failed finding room with id=$roomId") Timber.d(exception, "Failed finding room with id=$roomId.")
return null return null
} }
} }

View file

@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.media.AudioInfo 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.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.ImageInfo
@ -32,17 +31,19 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.StateEventType
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.room.roomMembers
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.timeline.item.event.EventType
import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.core.toProgressWatcher
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.room.location.toInner
import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline
import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow
import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -62,6 +63,7 @@ import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixRoom( class RustMatrixRoom(
override val sessionId: SessionId, override val sessionId: SessionId,
private val roomListItem: RoomListItem, private val roomListItem: RoomListItem,
@ -74,6 +76,11 @@ class RustMatrixRoom(
override val roomId = RoomId(innerRoom.id()) override val roomId = RoomId(innerRoom.id())
// Create a dispatcher for all room methods...
private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32)
//...except getMember methods as it could quickly fill the roomDispatcher...
private val roomMembersDispatcher = coroutineDispatchers.io.limitedParallelism(8)
private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId") private val roomCoroutineScope = sessionCoroutineScope.childScope(coroutineDispatchers.main, "RoomScope-$roomId")
private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown) private val _membersStateFlow = MutableStateFlow<MatrixRoomMembersState>(MatrixRoomMembersState.Unknown)
private val isInit = MutableStateFlow(false) private val isInit = MutableStateFlow(false)
@ -83,7 +90,7 @@ class RustMatrixRoom(
matrixRoom = this, matrixRoom = this,
innerRoom = innerRoom, innerRoom = innerRoom,
roomCoroutineScope = roomCoroutineScope, roomCoroutineScope = roomCoroutineScope,
coroutineDispatchers = coroutineDispatchers dispatcher = roomDispatcher
) )
} }
@ -105,7 +112,7 @@ class RustMatrixRoom(
timelineLimit = null timelineLimit = null
) )
roomListItem.subscribe(settings) roomListItem.subscribe(settings)
roomCoroutineScope.launch(coroutineDispatchers.computation) { roomCoroutineScope.launch(roomDispatcher) {
innerRoom.timelineDiffFlow { initialList -> innerRoom.timelineDiffFlow { initialList ->
_timeline.postItems(initialList) _timeline.postItems(initialList)
}.onEach { }.onEach {
@ -175,7 +182,7 @@ class RustMatrixRoom(
override val activeMemberCount: Long override val activeMemberCount: Long
get() = innerRoom.activeMembersCount().toLong() get() = innerRoom.activeMembersCount().toLong()
override suspend fun updateMembers(): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun updateMembers(): Result<Unit> = withContext(roomMembersDispatcher) {
val currentState = _membersStateFlow.value val currentState = _membersStateFlow.value
val currentMembers = currentState.roomMembers() val currentMembers = currentState.roomMembers()
_membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers) _membersStateFlow.value = MatrixRoomMembersState.Pending(prevRoomMembers = currentMembers)
@ -189,20 +196,20 @@ class RustMatrixRoom(
} }
override suspend fun userDisplayName(userId: UserId): Result<String?> = override suspend fun userDisplayName(userId: UserId): Result<String?> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.memberDisplayName(userId.value) innerRoom.memberDisplayName(userId.value)
} }
} }
override suspend fun userAvatarUrl(userId: UserId): Result<String?> = override suspend fun userAvatarUrl(userId: UserId): Result<String?> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.memberAvatarUrl(userId.value) innerRoom.memberAvatarUrl(userId.value)
} }
} }
override suspend fun sendMessage(message: String): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun sendMessage(message: String): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
messageEventContentFromMarkdown(message).use { content -> messageEventContentFromMarkdown(message).use { content ->
runCatching { runCatching {
@ -211,7 +218,7 @@ class RustMatrixRoom(
} }
} }
override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun editMessage(originalEventId: EventId?, transactionId: String?, message: String): Result<Unit> = withContext(roomDispatcher) {
if (originalEventId != null) { if (originalEventId != null) {
runCatching { runCatching {
innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId) innerRoom.edit(/* TODO use content */ message, originalEventId.value, transactionId)
@ -224,7 +231,7 @@ class RustMatrixRoom(
} }
} }
override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun replyMessage(eventId: EventId, message: String): Result<Unit> = withContext(roomDispatcher) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
// val content = messageEventContentFromMarkdown(message) // val content = messageEventContentFromMarkdown(message)
runCatching { runCatching {
@ -232,50 +239,50 @@ class RustMatrixRoom(
} }
} }
override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(coroutineDispatchers.io) { override suspend fun redactEvent(eventId: EventId, reason: String?) = withContext(roomDispatcher) {
val transactionId = genTransactionId() val transactionId = genTransactionId()
runCatching { runCatching {
innerRoom.redact(eventId.value, reason, transactionId) innerRoom.redact(eventId.value, reason, transactionId)
} }
} }
override suspend fun leave(): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun leave(): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.leave() innerRoom.leave()
} }
} }
override suspend fun acceptInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun acceptInvitation(): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.acceptInvitation() innerRoom.acceptInvitation()
} }
} }
override suspend fun rejectInvitation(): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun rejectInvitation(): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.rejectInvitation() innerRoom.rejectInvitation()
} }
} }
override suspend fun inviteUserById(id: UserId): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun inviteUserById(id: UserId): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.inviteUserById(id.value) innerRoom.inviteUserById(id.value)
} }
} }
override suspend fun canInvite(): Result<Boolean> = withContext(coroutineDispatchers.io) { override suspend fun canInvite(): Result<Boolean> = withContext(roomMembersDispatcher) {
runCatching { runCatching {
innerRoom.member(sessionId.value).use(RoomMember::canInvite) innerRoom.member(sessionId.value).use(RoomMember::canInvite)
} }
} }
override suspend fun canSendStateEvent(type: StateEventType): Result<Boolean> = withContext(coroutineDispatchers.io) { override suspend fun canSendStateEvent(type: StateEventType): Result<Boolean> = withContext(roomMembersDispatcher) {
runCatching { runCatching {
innerRoom.member(sessionId.value).use { it.canSendState(type.map()) } innerRoom.member(sessionId.value).use { it.canSendState(type.map()) }
} }
} }
override suspend fun canSendEvent(type: MessageEventType): Result<Boolean> = withContext(coroutineDispatchers.io) { override suspend fun canSendEvent(type: MessageEventType): Result<Boolean> = withContext(roomMembersDispatcher) {
runCatching { runCatching {
innerRoom.member(sessionId.value).use { it.canSendMessage(type.map()) } innerRoom.member(sessionId.value).use { it.canSendMessage(type.map()) }
} }
@ -305,13 +312,13 @@ class RustMatrixRoom(
} }
} }
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.toggleReaction(key = emoji, eventId = eventId.value) innerRoom.toggleReaction(key = emoji, eventId = eventId.value)
} }
} }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
roomContentForwarder.forward(fromRoom = innerRoom, eventId = eventId, toRoomIds = roomIds) roomContentForwarder.forward(fromRoom = innerRoom, eventId = eventId, toRoomIds = roomIds)
}.onFailure { }.onFailure {
@ -320,14 +327,14 @@ class RustMatrixRoom(
} }
override suspend fun retrySendMessage(transactionId: String): Result<Unit> = override suspend fun retrySendMessage(transactionId: String): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.retrySend(transactionId) innerRoom.retrySend(transactionId)
} }
} }
override suspend fun cancelSend(transactionId: String): Result<Unit> = override suspend fun cancelSend(transactionId: String): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.cancelSend(transactionId) innerRoom.cancelSend(transactionId)
} }
@ -335,40 +342,40 @@ class RustMatrixRoom(
@OptIn(ExperimentalUnsignedTypes::class) @OptIn(ExperimentalUnsignedTypes::class)
override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> = override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList()) innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList())
} }
} }
override suspend fun removeAvatar(): Result<Unit> = override suspend fun removeAvatar(): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.removeAvatar() innerRoom.removeAvatar()
} }
} }
override suspend fun setName(name: String): Result<Unit> = override suspend fun setName(name: String): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.setName(name) innerRoom.setName(name)
} }
} }
override suspend fun setTopic(topic: String): Result<Unit> = override suspend fun setTopic(topic: String): Result<Unit> =
withContext(coroutineDispatchers.io) { withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.setTopic(topic) innerRoom.setTopic(topic)
} }
} }
private suspend fun fetchMembers() = withContext(coroutineDispatchers.io) { private suspend fun fetchMembers() = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.fetchMembers() innerRoom.fetchMembers()
} }
} }
override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason)
if (blockUserId != null) { if (blockUserId != null) {
@ -383,7 +390,7 @@ class RustMatrixRoom(
description: String?, description: String?,
zoomLevel: Int?, zoomLevel: Int?,
assetType: AssetType?, assetType: AssetType?,
): Result<Unit> = withContext(coroutineDispatchers.io) { ): Result<Unit> = withContext(roomDispatcher) {
runCatching { runCatching {
innerRoom.sendLocation( innerRoom.sendLocation(
body = body, body = body,

View file

@ -16,9 +16,9 @@
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -41,7 +41,7 @@ import timber.log.Timber
internal class RustRoomSummaryDataSource( internal class RustRoomSummaryDataSource(
private val roomListService: RoomListService, private val roomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
coroutineDispatchers: CoroutineDispatchers, dispatcher: CoroutineDispatcher,
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(), roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) : RoomSummaryDataSource { ) : RoomSummaryDataSource {
@ -53,7 +53,7 @@ internal class RustRoomSummaryDataSource(
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true) private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
init { init {
sessionCoroutineScope.launch(coroutineDispatchers.computation) { sessionCoroutineScope.launch(dispatcher) {
val allRooms = roomListService.allRooms() val allRooms = roomListService.allRooms()
allRooms allRooms
.observeEntriesWithProcessor(allRoomsListProcessor) .observeEntriesWithProcessor(allRoomsListProcessor)

View file

@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.impl.timeline package io.element.android.libraries.matrix.impl.timeline
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimeline
@ -25,6 +24,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage
import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -45,7 +45,7 @@ class RustMatrixTimeline(
roomCoroutineScope: CoroutineScope, roomCoroutineScope: CoroutineScope,
private val matrixRoom: MatrixRoom, private val matrixRoom: MatrixRoom,
private val innerRoom: Room, private val innerRoom: Room,
private val coroutineDispatchers: CoroutineDispatchers, private val dispatcher: CoroutineDispatcher,
) : MatrixTimeline { ) : MatrixTimeline {
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> =
@ -109,13 +109,13 @@ class RustMatrixTimeline(
} }
} }
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = withContext(dispatcher) {
runCatching { runCatching {
innerRoom.fetchDetailsForEvent(eventId.value) innerRoom.fetchDetailsForEvent(eventId.value)
} }
} }
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(coroutineDispatchers.io) { override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result<Unit> = withContext(dispatcher) {
runCatching { runCatching {
Timber.v("Start back paginating for room ${matrixRoom.roomId} ") Timber.v("Start back paginating for room ${matrixRoom.roomId} ")
val paginationOptions = PaginationOptions.UntilNumItems( val paginationOptions = PaginationOptions.UntilNumItems(
@ -131,7 +131,7 @@ class RustMatrixTimeline(
} }
} }
override suspend fun sendReadReceipt(eventId: EventId) = withContext(coroutineDispatchers.io) { override suspend fun sendReadReceipt(eventId: EventId) = withContext(dispatcher) {
runCatching { runCatching {
innerRoom.sendReadReceipt(eventId = eventId.value) innerRoom.sendReadReceipt(eventId = eventId.value)
} }

View file

@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class FakeMatrixClient( class FakeMatrixClient(
@ -72,11 +73,11 @@ class FakeMatrixClient(
return findDmResult return findDmResult
} }
override suspend fun ignoreUser(userId: UserId): Result<Unit> { override suspend fun ignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return ignoreUserResult return ignoreUserResult
} }
override suspend fun unignoreUser(userId: UserId): Result<Unit> { override suspend fun unignoreUser(userId: UserId): Result<Unit> = simulateLongTask {
return unignoreUserResult return unignoreUserResult
} }

View file

@ -239,7 +239,7 @@ class FakeMatrixRoom(
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia(progressCallback) override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia(progressCallback)
override suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit> = simulateLongTask { override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = simulateLongTask {
forwardEventResult forwardEventResult
} }

View file

@ -19,10 +19,8 @@ package io.element.android.libraries.matrix.ui.di
import com.squareup.anvil.annotations.ContributesTo import com.squareup.anvil.annotations.ContributesTo
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.ui.media.LoggedInImageLoaderFactory import io.element.android.libraries.matrix.ui.media.LoggedInImageLoaderFactory
import io.element.android.libraries.matrix.ui.media.NotLoggedInImageLoaderFactory
@ContributesTo(SessionScope::class) @ContributesTo(SessionScope::class)
interface MatrixUIBindings { interface MatrixUIBindings {
fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory fun loggedInImageLoaderFactory(): LoggedInImageLoaderFactory
fun notLoggedInImageLoaderFactory(): NotLoggedInImageLoaderFactory
} }

View file

@ -123,7 +123,7 @@ class NotificationFactory @Inject constructor(
val roomMeta = roomNotifications.filterIsInstance<RoomNotification.Message>().map { it.meta } val roomMeta = roomNotifications.filterIsInstance<RoomNotification.Message>().map { it.meta }
val invitationMeta = invitationNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta } val invitationMeta = invitationNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
val simpleMeta = simpleNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta } val simpleMeta = simpleNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
val fallbackMeta = simpleNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta } val fallbackMeta = fallbackNotifications.filterIsInstance<OneShotNotification.Append>().map { it.meta }
return when { return when {
roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed
else -> SummaryNotification.Update( else -> SummaryNotification.Update(

View file

@ -106,7 +106,7 @@ class PendingIntentFactory @Inject constructor(
fun createDismissEventPendingIntent(sessionId: SessionId, roomId: RoomId, eventId: EventId): PendingIntent { fun createDismissEventPendingIntent(sessionId: SessionId, roomId: RoomId, eventId: EventId): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java) val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = actionIds.dismissEvent intent.action = actionIds.dismissEvent
intent.data = createIgnoredUri("deleteEvent/$sessionId/$roomId") intent.data = createIgnoredUri("deleteEvent/$sessionId/$roomId/$eventId")
intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value) intent.putExtra(NotificationBroadcastReceiver.KEY_SESSION_ID, sessionId.value)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value) intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId.value)
intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId.value) intent.putExtra(NotificationBroadcastReceiver.KEY_EVENT_ID, eventId.value)

View file

@ -22,10 +22,8 @@ import io.element.android.libraries.matrix.api.tracing.TracingConfigurations
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.Executors
object Singleton { object Singleton {
@ -39,6 +37,5 @@ object Singleton {
io = Dispatchers.IO, io = Dispatchers.IO,
computation = Dispatchers.Default, computation = Dispatchers.Default,
main = Dispatchers.Main, main = Dispatchers.Main,
diffUpdateDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
) )
} }

View file

@ -37,12 +37,10 @@ fun TestScope.testCoroutineDispatchers(
io = UnconfinedTestDispatcher(testScheduler), io = UnconfinedTestDispatcher(testScheduler),
computation = UnconfinedTestDispatcher(testScheduler), computation = UnconfinedTestDispatcher(testScheduler),
main = UnconfinedTestDispatcher(testScheduler), main = UnconfinedTestDispatcher(testScheduler),
diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler),
) )
false -> CoroutineDispatchers( false -> CoroutineDispatchers(
io = StandardTestDispatcher(testScheduler), io = StandardTestDispatcher(testScheduler),
computation = StandardTestDispatcher(testScheduler), computation = StandardTestDispatcher(testScheduler),
main = StandardTestDispatcher(testScheduler), main = StandardTestDispatcher(testScheduler),
diffUpdateDispatcher = StandardTestDispatcher(testScheduler),
) )
} }

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7ca068387cff8faf728a989488e7c4b5b07983c4b24162ff82a14fd90b82d05
size 20609

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e1599be0c5a37083c015378ee47a78a90dcf670f4df5d85dd25d39f4b2edbdf6
size 21147